
如果你维护过 AI Agent 的线上服务一定经历过这种时刻日志里一切正常监控面板上延迟和错误率都在合理范围但 Agent 就是不说话。用户等了两分钟页面转圈最后超时。或者更隐蔽——它看似把任务完成了但答案是在硬凑工具调用根本没生效。这种沉默和假完成比报错更让人抓狂。这就是我做 ANOLISA AgentSight 的初心让 Agent 的每一次沉默都无处躲藏。简单说这是一套面向 Agent 运行时行为观测与诊断的方案专门盯着那些传统监控看不见的安静失败。它不是又一个 APM 工具而是把 Agent 的思考、决策、工具调用、记忆读写全部变成可检索、可回放、可评测的数据。这篇文章我会把整套设计思路、埋点方案、常见坑都和盘托出适合正在做 Agent 应用开发的工程师也非常适合那些被线上 Agent 玄学问题折磨的运维和测试同学。1. 先搞明白Agent 的沉默到底意味着什么1.1 沉默的五种典型形态一开始我以为沉默只是模型没输出后来把几百条故障现场翻出来复盘才发现它至少有五种完全不同的形态处理方式也完全不一样。第一种是空转等待。模型在推理但这一步特别慢前端只显示正在思考没有流式输出。用户以为卡死直接刷新结果请求中断重试之后又带来重复调用。第二种是静默失败。最常见也最可恨。工具调用明明抛了异常但 Agent 框架吞掉了异常或者大模型收到了错误返回后选择假装没看到继续编造一个合理的结果。用户感知不到错误只会觉得结果有点怪。第三种是上下文塌缩。长对话里 token 超限被截断Agent 把任务目标、用户偏好、之前的中间结果全忘了开始泛泛而谈或重复提问。这种往往不报错但回答质量断崖式下降。第四种是死循环。模型反复调用同一个工具、拿到同样的结果、再次调用循环十几轮。从单次请求看每次都是成功的但整体任务永远结束不了。第五种是幻觉式完成。这个最阴险。Agent 没有真正执行工具只是按照训练记忆脑补了一个结果然后告诉你任务完成。外部表现是流程走完了实际结果全是编的。如果你在运营一个 Agent 类产品这五种情况几乎每天都会遇到。而且它们有个共同特征传统监控指标完全无感。HTTP 状态码是 200响应时间在正常范围内存 CPU 都没有异常。问题出在语义层和行为层。1.2 为什么通用监控救不了 Agent普通的 APM 和日志系统监控的是系统是否活着关注 CPU、内存、错误率、P99 延迟。但 Agent 的本质是一个自主决策循环感知上下文、推理决策、调用工具、观察结果、再次推理。这个循环里任何一环出问题都不会直接表现为系统崩溃而是表现为行为异常。举个例子。一个 Agent 调用了天气 APIAPI 返回了一个 429 限流错误。如果框架没有把这个错误透传给大模型大模型就会基于不完整的观察继续推理最后给你一个今天适合出行的错误建议。系统层面看这次请求是成功的业务层面看这完全是一次失败。也就是说通用监控看的是这台机器做了什么而 Agent 的可观测性需要回答的是这个智能体在什么状态下、基于什么信息、做了哪个决策、为什么这样做。这是两个维度。所以 ANOLISA AgentSight 从一开始就不是按传统 APM 的思路来设计而是按行为记录仪的思路来设计。不仅要记录 Agent 说了什么、做了什么还要记录它看到了什么、想到了什么、忽略了什么。2. AgentSight 的整体设计思路2.1 从进程监控到行为监控的转变在设计 ANOLISA AgentSight 时我参照了分布式链路追踪的思路但没有直接套用。传统 Trace 解决的是请求经过哪些服务而 Agent 的每次请求可能在一次会话里经历几十次模型调用、上百次工具调用、多次记忆读写而且这些调用之间存在复杂的依赖和分支关系。用传统的 span 树去刻画会非常臃肿。我最后定下的核心模型是四层记录意图层Intent用户这次会话的目标是什么Agent 自己理解的目标是什么。推理层Reasoning每次大模型调用的输入、输出、消耗的 token、温度等参数以及耗时。行动层Action每次工具调用的名称、参数、返回值、耗时、是否成功。记忆层Memory上下文窗口中关键内容的分布哪些信息被召回、哪些被截断、长期记忆和短期记忆的读写结果。这四层数据组合起来就能还原一个 Agent 在某个任务里完整的思考 - 行动 - 观察循环。任何一环出了偏差都能定位到具体位置。更重要的一点是这些记录不是事后补的而是在运行时实时采集并写入统一的链路存储。每一条记录都带着全局唯一的trace_id能在整个会话生命周期内把所有事件串联起来。2.2 核心模块拆解整个系统分五个模块每个模块解决一类沉默问题。第一个是链路追踪模块。负责生成和采集上面的四层记录把每个 Agent 决策步骤串成一条完整的时间线。这是排查问题的基础。当用户说刚才那个回答不对你能直接拉出这条回复背后完整的决策链路看到它在哪一步开始偏的。第二个是异常行为检测模块。专门盯着五种沉默形态。它有几个比较实用的检测器无输出计时器、重复调用检测器、上下文水位计、工具失败消化检测器。这些检测器不是简单阈值判断而是结合任务类型做了调整。比如对代码生成类任务允许的空转时间就更长对信息查询类任务重复调用超过三次就报警。第三个是记忆体检模块。每次大模型调用结束后分析上下文窗口的关键信息存量用户原始目标是否还在窗口内、最近的工具结果是否被截断、长期记忆中有哪些条目被召回。如果用户的目标被挤出窗口立刻标记为上下文塌缩风险。第四个是评测回放模块。把每一次线上真实故障沉淀成评测用例加入回归测试集。这个模块特别重要行业里常说的 agent 评测集构建不能只靠人工写题线上真实的生产故障是最好的素材。我把这些故障场景整理成已知问题画像每次 Agent 框架升级或 prompt 调整都跑一遍回放防止同一个坑反复踩。第五个是成本与安全审计模块。记录每次调用的模型、token 数、工具权限范围、数据流向。这既是成本管控的手段也是 Agent 安全问题排查的抓手。如果有人发现 Agent 调用了不该调用的工具或者数据被发送到了不该发的地方在审计日志里能一目了然。3. 实操给 Agent 装上行为记录仪3.1 统一埋点协议真正落地的时候最麻烦的不是系统本身而是埋点。市面上的 Agent 框架五花八门LangChain、Dify、CrewAI、自研框架各有各的抽象。如果每个框架写一套采集代码根本维护不动。我的做法是定义一套统一埋点协议然后为每个框架写一个适配层。核心数据结构大概长这样{ trace_id: a8f0c2b1e5, parent_span_id: root, span_id: sp_001, type: tool_call, agent_id: agent_weather_v3, session_id: session_88, timestamp: 2025-06-12T14:03:21.882Z, duration_ms: 326, data: { tool_name: get_weather, arguments: {city: 北京, date: 2025-06-13}, result: {\code\: 429, \message\: \rate limited\}, status: error, error_type: rate_limit, error_handled_by_llm: true } }关键点是type字段取值只有四种intent、reasoning、tool_call、memory_op。这样无论底层是哪个框架上层系统都能统一处理。每个 span 都带parent_span_id能还原调用树。埋点采集用异步队列不阻塞主流程。我用的是 Python 的contextvars来传递trace_id这样就算你的代码里有异步任务切换链路也不会断。这里要特别注意如果你的 Agent 代码里有asyncio.gather并发调用多个工具一定要把trace_id显式传给子任务否则子任务的埋点就变成孤儿节点了。import contextvars import asyncio from agent_sight import tracer current_trace contextvars.ContextVar(current_trace, defaultNone) async def run_agent(task): trace_id tracer.new_trace(task) current_trace.set(TraceContext(trace_idtrace_id, session_idtask.session_id)) try: result await execute_plan(task) tracer.end_trace(trace_id, result) return result except Exception as e: tracer.record_exception(trace_id, e) raise3.2 拦截三类关键事件埋点协议定好了接下来要解决在哪埋的问题。经过多轮迭代我总结出三个必须拦截的关键点。第一是大模型调用前后。这是所有 Agent 系统最核心的事件没有之一。调用前记录prompt的完整内容、模型参数、包括原始输入和系统提示词调用后记录response、token 消耗、延迟。这里有个经验很多人只记录prompt不记录response导致排查问题时无法确认模型到底输出过什么这是大忌。response一定要存而且要存原始内容不要只存提取后的结构化字段。第二是工具调度中心。无论你用的是什么框架最终所有工具调用都会经过某个 dispatch 函数。在这个函数里做拦截能覆盖所有工具。记录工具名、参数、返回结果、异常堆栈。额外要记录一个字段error_handled_by_llm表示这个错误是否被传递给了大模型处理。这个字段是我排查静默失败问题的关键。第三是记忆读写接口。不同框架对记忆的实现差别很大有的用向量库有的用缓存有的直接把历史消息塞进上下文。统一的做法是封装一个memory_sink接口所有读写操作强制走这个接口然后在接口内部埋点。记录读到了哪些内容、这些内容占了多少 token、被截断的边界在哪。3.3 静默状态检测的判据与阈值埋点数据有了如何判定沉默我把判定逻辑做成一个配置化的规则引擎下面的阈值是我在真实项目里调试出来的经验值大家可以根据自己的业务调整。异常类型检测条件建议阈值说明空转等待从 LLM 调用到首个 token 输出耗时 30 秒模型正常推理通常 5~15 秒死循环相同工具、相同参数连续调用次数 3 次超过 3 次基本是陷入循环上下文塌缩用户原始目标在窗口中的 token 位置超过窗口 70% 截断低于 30% 时目标被挤出的风险极高静默失败工具 error 且 error_handled_by_llm 为 false任何一次都记录不报警但要标记风险幻觉式完成工具调用返回异常后LLM 回复仍含确定结论无法自动判定配合评测集人工打分关于空转检测我要多说两句。30 秒不是拍脑袋定的。我统计过多种模型的推理时间分布短文本生成任务的 P95 在 20 秒以内而复杂规划任务可能要到 40 秒。所以这里的判据要跟任务类型挂钩不能一刀切。最好的做法是引入自适应阈值根据同一 agent 过去 100 次调用的耗时分布动态计算阈值取 P95 加一个缓冲。上下文塌缩检测我强烈建议做。这个问题的隐蔽性极高而且一旦发生Agent 就会变成失忆患者回答得彬彬有礼但完全不靠谱。检测方式很简单定期检查用户原始目标对应的文本是否还在上下文窗口中。如果目标文本已经因为 token 超限被截断立刻触发告警。3.4 评测集与回归回路AgentSight 最后落地的闭环是评测集。我见过太多团队Agent 上线之后全靠线上用户反馈来发现问题这太被动了。正确的做法是把线上故障变成测试用例。具体操作分三步。第一步每确认一次线故障就把当时的 trace 完整导出包括输入的 prompt、工具的返回值、模型的输出存成一个 json 文件。第二步把这份 trace 转成评测用例核心是给出同样的输入回放同样的工具返回观察 Agent 是否做出正确的决策。第三步把这些用例放进一个持续执行的回归任务每次框架升级、prompt 调整、工具变更都要跑一遍。这个做法有很大价值。举个例子我曾经遇到一个 bug新版本升级后Agent 在处理日期格式时突然频繁报错但线上日志一切正常。最终靠评测集回放定位到是底层框架把日期参数从字符串改成了时间戳对象导致工具解析失败。这样的问题纯靠人工观察几乎不可能发现。我实际验证过一个运行了三个月的业务 Agent靠这种方式积累了 200 多个评测用例。这些用例比任何手工构造的测试集都更能反映真实用户场景。4. 真实踩坑实录那些让 Agent 沉默的凶手4.1 问题速查表先放一张速查表都是我在真实项目里踩过的坑。现象根因排查方向Agent 长时间无输出前端转圈模型推理慢或网络连接未超时检查 LLM 调用耗时、流式响应是否开启上报rpc error (-1): empty sid and service nameRPC 会话标识丢失检查 SDK 初始化参数、跨进程传递的 sid 字段工具报错但用户没察觉框架吞掉异常LLM 未收到错误信息检查工具调度层异常处理逻辑回答开始泛化失去针对性上下文被截断目标信息被挤出检查 token 占用、窗口水位同样的工具调用反复执行多次模型未从返回结果中得到有效信息检查工具返回结构是否可解析延迟正常但整体任务耗时巨大循环调用次数过多检查重复调用检测器日志更换模型后效果骤降prompt 与模型能力不匹配回放评测集对比前后行为4.2 深入聊聊 empty sid 报错你光看报错信息会一头雾水empty sid and service name 不是标准 HTTP 错误也不是 LLM API 返回的错误它来自底层 RPC 通信层。我遇到的情况是这样的某次版本迭代后Agent 在调用内部服务时开始随机出现这个报错而且一旦出现整个请求就挂起直到超时。排查过程走了不少弯路。一开始怀疑是网络问题重试了好几次都没用。后来通过 ANOLISA 的链路追踪发现凡是报错的请求都经过了新加的一个负载均衡中间层。而这个中间件在转发请求时把 RPC 请求头里的sid字段丢掉了。RPC 框架的sid是会话标识相当于一次远程调用的身份证。如果为空服务端无法把响应关联到正确的调用方就会直接拒绝。这个问题的根因不是 Agent 本身而是基础设施变更导致上下文信息丢失。但传统监控根本发现不了因为它表现为服务间调用偶发失败很容易被归类为网络抖动。靠链路追踪看到sid在跨服务时消失问题才彻底定位。这个案例给我的经验是Agent 程序跑得再好底层通信层出问题也会导致沉默而且这种沉默最隐蔽——因为它不是 Agent 主动犯错而是外部输入被截断。4.3 工具调用安静失败的元凶工具静默失败是我最常处理的线上问题没有之一。它的典型表现是用户问一个问题Agent 需要调用一个搜索 API但 API 因为认证过期返回了 401框架没有把这错误报告给大模型。结果大模型就凭自己的记忆编了一个答案。这种问题的根源基本都在工具封装层。很多开发者在写工具函数时有个坏习惯把所有异常 catch 住然后返回一个None或者空字符串。在他们看来这避免了程序崩溃但从 Agent 的行为看这等于剥夺了大模型的知情权让它在一无所知的情况下继续做决策。标准的做法是工具函数只能抛出异常或者在返回值中显式携带error和error_type字段绝不允许把一个失败的工具调用伪装成成功结果。在 AgentSight 的工具调用记录里我专门设计了status字段取值为success、error、partial。其中partial表示部分成功比如搜索返回了结果但少了一页。这样后续无论是人工排查还是自动化检测都能一眼看出工具的真实状态。另一个隐藏很深的坑是超时设置。有次线上一个问题Agent 某次工具调用特别慢从 2 秒涨到了 8 秒而请求配置的超时是 10 秒。每次都没超时但整体响应时间越来越不可控。后来把工具超时的默认值调到了 5 秒并且把超时错误显式返回给模型模型就能自己决定换个方案而不是傻等。对 Agent 来说快速失败并让模型感知错误永远比慢慢磨叽要好。4.4 上下文膨胀导致的推理假死最后分享一个特别反直觉的案例。某次线上用户反馈长对话超过 30 轮后Agent 的响应速度从 3 秒劣化到 40 秒而且回答质量明显下降。从监控看token 消耗在上涨但没有到硬性限制。表面上一切都正常。但我把链路追踪打开发现了一个诡异的现象模型推理的耗时在指数级增加。原因在于没有做消息裁剪。每轮对话都把完整历史拼接到 prompt 里发送给模型导致 token 数爆炸。模型要处理的信息量太大推理时间自然上涨。更严重的是当上下文接近模型的窗口上限模型对早期用户目标的注意力会被稀释。它不是在思考更久而是在海量信息中迷失方向。这就是我前面说的上下文塌缩的温床。解法也很直接显式管理上下文窗口。把用户原始目标、关键约束、最近一次工具结果标记为永久保留区把早期对话历史压缩成摘要。我审计了线上用户的对话记录发现 80% 的早期对话内容都可以压缩而不影响任务完成。这个改动上线后长对话的 P95 延迟从 40 秒降到了 8 秒上下文塌缩的风险也大幅下降。5. 在团队里落地 AgentSight 的几点经验技术上讲完了最后唠唠落地过程中的一些团队协作经验这部分往往是文档不会写的。第一Agent 可观测性的价值要让测试同学先看到。我一开始给研发团队演示了很多花哨的图表但大家反馈一般。真正产生影响力的是我把一个返工了三天都查不出来的问题用 AgentSight 五分钟定位到了根因。从那天起测试同学开始主动要求接入。先解决一个让所有人头痛的疑难杂症比十场宣讲都有用这是我踩过的坑。第二埋点 JSON 的字段一定要强制规范。刚开始接入时各个团队自由发挥有人把工具参数放data里有人放meta里导致后续做统计分析要对齐字段名改老半天。后来我把协议版本号和字段校验加进了提交 CI 环节格式不对直接不让发布。这能省下后来大量的时间。第三评测集要当成资产来维护。不要让评测集只躺在 CI 里吃灰要给每个用例打上标签涉及哪个 Agent、哪个工具、哪类问题。这样当你有新需求或者换模型时能精准挑选相关用例进行验证而不是盲目跑全量。我用的是按业务模块分目录然后通过标签自动组装回归集。另外在运行 AgentSight 的过程中我发现它天然就是成本分析与 Agent 安全工作的数据底座。以前我们回答不了这个 Agent 为什么比预期贵这种问题现在拉出reasoning层的 token 记录一算便知以前审计某次数据流向要手动翻代码现在直接看审计模块的链路就能回答这次调用用了哪个工具、访问了哪个系统、传了哪些字段。这些都是当初设计时没想到的收获。我个人在实际操作中有个习惯每个周末把这个周期的所有沉默告警拉出来按根因分个类。做的次数多了你会对这套系统的脾气非常了解甚至能从告警的形态猜出这周大概哪个模块又动了配置。让 Agent 的每一次沉默都无处躲藏本质上不是要把 Agent 变成一台永不犯错的机器而是让它的每一个决策过程都透明到可以被审视让真正的问题浮出水面而不是沉在水底等用户来发现。这个思路无论是自研还是用第三方方案都值得每一位做 Agent 应用的开发者认真对待。