ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

AI Coding Agent可观测性与自愈机制实战:从装死到闭环

AI Coding Agent可观测性与自愈机制实战:从装死到闭环 1. 当Agent开始装死我们才意识到它缺了点什么你有没有遇到过这种情况让AI coding agent帮你重构一个模块它信誓旦旦地说已完成修改你一看diff改是改了但把隔壁两个文件的import全删了。或者更隐蔽的——它跑了一个测试命令返回码是0但实际测试根本没执行因为路径写错了shell静默失败了。你问它怎么回事它说测试通过任务完成。这不是模型能力问题这是可观测性缺失的问题。Agent在执行和验证之间有一道巨大的裂缝它能发出动作但看不见动作的真实后果。就像一个蒙着眼睛修水管的人拧了扳手听见水声停了就以为修好了——实际上可能是总阀被邻居关了。这篇东西不是要讲什么高深理论而是把我自己在给coding agent搭可观测层和自愈机制时踩过的坑、试过的方案、以及几个真正能跑起来的开源思路整理出来。适合已经在用Claude Code、Cursor、Aider或者自己搭agent loop的人看也适合想从零做一个带反馈闭环的agent系统的开发者。核心就一件事怎么让agent不仅能干活还能知道自己干得对不对以及干错了怎么自己拐回来。2. 可观测不是加日志是给Agent建一套体感系统2.1 为什么传统日志对Agent几乎没用大多数人第一反应是加日志嘛。我一开始也这么干在agent每一步后面打一行print(f[DEBUG] step {i}: {action})。跑了两天就放弃了——日志文件几万行出了错根本不知道从哪看起。更致命的是日志是给人看的不是给agent自己看的。Agent需要的是结构化的、可判断的、能直接触发决策的信号而不是一堆文本。这里有个认知转变很关键可观测性的服务对象有两个——你和agent本身。给人看的部分是事后复盘用的trace给agent看的部分是实时决策用的feedback signal。这两者数据结构完全不同。前者可以是非结构化的span树后者必须是紧凑的、带语义标签的、能直接喂回prompt的状态对象。我后来把agent的每一步抽象成一个StepRecord大概长这样dataclass class StepRecord: step_id: int action_type: str # edit_file | run_cmd | read_file | think action_payload: dict expected_outcome: str # agent自己声明的预期 actual_outcome: dict # 实际观测到的结果 exit_code: int | None stdout_digest: str # 截断摘要后的输出 stderr_digest: str side_effects: list # 文件变更、进程状态等 timestamp: float duration_ms: int关键字段是expected_outcome和actual_outcome的对比。Agent在执行前必须声明我预期会发生什么执行后系统填充实际发生了什么两者的差异就是可观测性的核心信号。没有这个对比日志就只是流水账。2.2 三个必须观测的维度文件系统、进程、语义我试过很多观测点最后收敛到三个维度缺一不可。文件系统维度是最容易被忽略的。Agent改文件你不能只看它说改了什么要看文件系统实际发生了什么。我用的是watchdog库做inotify监听配合git的diff --stat做快照对比。每次agent声称修改了X文件系统自动跑一次git diff --name-only如果agent说的和实际改的对不上立刻标记为phantom_edit异常。这个机制帮我抓到过好几次agent幻觉编辑——它以为改了实际因为路径错误写到了别的地方。进程维度主要针对命令执行。subprocess.run的returncode只是最粗的信号。我额外采集进程实际运行时长对比预期、CPU/内存峰值判断是否真的在干活、子进程树判断有没有fork出意外的东西。有一次agent跑pytestreturncode是0但进程只跑了0.3秒——正常测试套件至少要跑十几秒。一查它跑的是pytest --collect-only根本没执行测试。如果只看returncode这个错误永远发现不了。语义维度是最难但最有价值的。Agent的输出文本里藏着它的自我认知。我用一个轻量级的规则小模型混合方案做语义抽取从agent的回复里提取我完成了X我验证了YZ应该没问题这类断言然后逐条去和实际观测结果对账。对不上的断言就是过度自信信号直接触发自愈流程。这个方案不需要大模型我用正则加一个几百M的小分类模型就够了延迟控制在50ms以内。2.3 用OpenTelemetry给Agent搭trace骨架如果你不想自己造轮子OpenTelemetry是目前最成熟的选择。它的span模型天然适合agent的嵌套执行结构一个task是一个root span每个step是一个child spanstep内部的工具调用是grandchild span。我实际接入的方式是给agent的每个action包一层OTel decoratorfrom opentelemetry import trace tracer trace.get_tracer(agent.core) def traced_action(action_type): def decorator(fn): def wrapper(self, *args, **kwargs): with tracer.start_as_current_span(faction.{action_type}) as span: span.set_attribute(action.type, action_type) span.set_attribute(action.input, str(args)[:500]) try: result fn(self, *args, **kwargs) span.set_attribute(action.status, ok) span.set_attribute(action.output_digest, str(result)[:500]) return result except Exception as e: span.set_attribute(action.status, error) span.record_exception(e) raise return wrapper return decorator好处是你可以用Jaeger或者Grafana Tempo直接看整个agent的执行树哪个step耗时异常、哪个step报错、step之间的依赖关系一目了然。更重要的是OTel的span context可以跨进程传播——如果你的agent会调用子agent或者外部工具trace能串起来。但OTel有个坑默认的batch exporter会丢数据。Agent执行快的时候span还没flush进程就结束了。我改成SimpleSpanProcessor加同步导出虽然性能差一点但数据完整性有保障。生产环境可以用BatchSpanProcessor配合force_flush()在关键节点手动刷。2.4 一个反直觉的经验观测粒度要粗到能决策细到能定位我一开始把观测做到极细每个函数调用都打点。结果agent的prompt里塞满了观测数据反而干扰了它的判断。后来我定了一个原则喂给agent的观测信号粒度必须是能直接触发一个决策的。比如文件X的第42行被修改太细文件X的修改引入了语法错误刚好文件X的修改导致测试Y失败就是最佳粒度。而给人看的trace可以细到函数级因为人可以做关联分析。这个区分很重要很多agent可观测方案失败就是因为没区分这两个消费者。3. 自愈不是重试是让Agent学会认错和换路3.1 重试为什么经常越试越糟最简单的自愈就是失败重试。我试过效果很差。Agent执行失败后重试往往用同样的方式再撞一次墙。因为它不知道自己为什么失败——错误信息在stderr里但它可能根本没读或者读了没理解。更糟的是重试污染。Agent第一次改文件改错了重试时在错误的基础上继续改越改越乱。我遇到过一次agent重试了7次把一个200行的文件改成了800行全是重复代码和注释掉的旧逻辑。所以自愈的第一步不是重试是**冻结现场归因**。失败后立刻做三件事保存当前文件系统快照git stash或者copy、提取错误信号、让agent基于错误信号重新规划。这三步做完再决定是重试、回滚还是换方案。3.2 基于预期-实际差异的自愈触发条件自愈不能瞎触发要有明确的触发条件。我总结了几类高价值的触发信号信号类型具体表现自愈动作编辑幻觉agent声称改了A实际改了B或没改回滚重新定位目标文件静默失败returncode0但输出为空/异常短重新执行加verbose参数断言冲突agent说测试通过但实际有fail强制读取测试输出重新判断循环检测连续3步action高度相似中断换策略提示副作用溢出修改了预期外的文件回滚溢出部分告警这些信号里循环检测最容易被忽略但最重要。Agent陷入循环时它自己往往意识不到。我的做法是维护一个最近10步的action embedding用余弦相似度检测。如果连续3步相似度超过0.9就强制注入一条系统消息你似乎在重复之前的操作请换一种思路或者说明你卡在哪里。3.3 回滚策略git stash不是万能的说到回滚很多人第一反应是git stash。但agent执行过程中可能产生未tracked的新文件git stash默认不处理这些。我用的是组合方案# 执行前 git add -A git stash push -m agent-checkpoint-$(date %s) # 或者更彻底的方式 cp -r workspace workspace.checkpoint.$(date %s)git stash的问题是它会改变工作区状态如果agent正在依赖某些未提交的改动stash后可能直接跑不起来。我后来改用影子目录快照每次agent执行前用rsync把workspace同步到一个.checkpoints/目录回滚时反向同步。这个方案对agent完全透明不影响它的执行环境。代价是磁盘占用。我的策略是保留最近5个checkpoint更早的自动清理。对于大仓库可以用rsync --link-dest做硬链接快照几乎不占额外空间。3.4 让Agent自己写失败复盘再重试这是我觉得最有效的一招失败后不让agent直接重试而是强制它先写一段复盘。复盘必须包含三个要素失败的直接原因、根本原因、下一步的不同做法。Prompt大概是这样上一步执行失败。在重试之前请先完成以下分析 1. 实际发生了什么基于提供的stderr和文件状态 2. 你之前的预期是什么差异在哪里 3. 根本原因是什么不要停留在表面 4. 下一步你会采取什么不同的做法为什么这次会成功 只有完成分析后才能执行下一步。这个机制的效果超出预期。Agent在写复盘的过程中经常自己就发现了问题——比如我假设文件用的是Python 3.10语法但实际环境是3.8。而且复盘文本会进入context后续步骤会参考它避免重复犯错。我统计过加了复盘机制后同一任务的agent步数平均减少了23%因为减少了无效重试。4. 几个能直接抄的开源方案与组合拳4.1 LangFuse给Agent做trace和eval的性价比之选LangFuse是我目前用得最多的开源可观测方案。它本来是给LLM应用做trace的但它的span模型和score机制非常适合agent场景。核心用法是给每个agent step打一个span然后用它的score API给step打分from langfuse import Langfuse langfuse Langfuse() trace langfuse.trace(namecoding_task, inputtask_description) span trace.span(namestep_edit_file, inputedit_payload) # ... 执行 ... span.end(outputactual_result) # 用规则或小模型打分 langfuse.score( trace_idtrace.id, nameedit_correctness, value1.0 if expected actual else 0.0, comment文件修改与预期一致 )好处是它自带Web UI能看到每个task的完整执行树和分数分布。我每周会看一次score的分布如果edit_correctness的均值下降说明agent的编辑能力在退化可能是prompt改动或者模型更新导致的。LangFuse的坑自托管版本的clickhouse依赖比较重小机器跑不动。如果只是个人用可以用它的cloud免费版或者退而求其次用SQLite做本地存储。4.2 OpenTelemetry Grafana适合已有可观测栈的团队如果你的团队已经在用Grafana那直接上OTel是最顺的。Agent的trace数据进Tempometrics进Prometheus日志进Loki然后在Grafana里做统一面板。我搭过一个面板核心指标就四个step成功率、平均step耗时、自愈触发率、自愈成功率。这四个指标能覆盖80%的agent健康度判断。比如自愈触发率突然飙升说明agent遇到了系统性问题自愈成功率下降说明自愈策略需要调整。OTel的语义约定semantic conventions里没有agent相关的标准我建议自定义一套attribute命名规范比如agent.step.type、agent.step.status、agent.selfheal.trigger。团队内统一就行不用等标准。4.3 用inotifygit hooks做轻量级文件观测如果你不想引入重型依赖watchdoggit的组合能覆盖大部分文件观测需求。我写过一个不到200行的FileObserver核心逻辑from watchdog.observers import Observer from watchdog.events import FileSystemEventHandler class AgentFileWatcher(FileSystemEventHandler): def __init__(self, expected_changes): self.expected set(expected_changes) self.actual set() def on_modified(self, event): if not event.is_directory: self.actual.add(event.src_path) def reconcile(self): phantom self.expected - self.actual unexpected self.actual - self.expected return {phantom: phantom, unexpected: unexpected}Agent执行前声明它要改哪些文件执行后用reconcile()对账。phantom是它说改了但没改的unexpected是它没说要改但改了的。两个集合都为空才算干净。这个方案的好处是零外部依赖坏处是inotify在容器环境里可能不工作取决于挂载方式。容器里可以用polling模式watchdog支持PollingObserver代价是延迟高一点。4.4 自愈策略的编排用状态机而不是if-else自愈逻辑如果写成if-else很快就会变成意大利面。我用的是一个简单的状态机NORMAL - (检测到异常) - DIAGNOSING - (归因完成) - ROLLING_BACK - RETRYING - NORMAL - (无法归因) - ESCALATING - HUMAN_INTERVENTION每个状态有明确的进入条件、执行动作、退出条件。用Python的transitions库或者自己写一个几十行的状态机都行。关键是状态转换必须可观测——每次转换都打一个span这样你能看到agent在自愈流程里卡在哪一步。我踩过的坑自愈不能无限循环。我设了一个max_selfheal_attempts3超过就升级到人工。有一次agent在一个自愈循环里转了3次都没出来第4次我强制中断发现它在反复回滚和重试同一个错误。如果没有这个上限它能转到天荒地老。5. 踩坑实录那些让我半夜爬起来改代码的瞬间5.1 观测数据污染了Agent的Context早期我把完整的stderr塞进agent的context想着信息越多越好。结果agent被一堆warning和deprecation信息干扰反而忽略了真正的error。更糟的是有些命令的stderr有几千行直接把context窗口撑爆了。后来我加了一个输出摘要层stderr先过一遍规则过滤去掉warning、deprecation、progress bar再截断到关键部分error前后各20行最后才喂给agent。这个摘要层用正则就能实现不需要模型。经验喂给agent的观测数据信噪比比信息量重要得多。宁可少给不可给杂。5.2 自愈触发了但Agent不知道有一次我加了自愈逻辑检测到异常后自动回滚了文件。但agent不知道发生了回滚它以为自己的修改还在下一步基于错误的假设继续执行。结果就是回滚了又改回去改回去又回滚死循环。修复方案很简单但容易忘任何自愈动作都必须显式通知agent。回滚后注入一条系统消息检测到异常已回滚到上一个检查点。你之前的修改已撤销请基于当前状态重新规划。这条消息必须进入agent的context否则它就是在盲跑。5.3 循环检测的误报我用action embedding做循环检测阈值设0.9。结果发现agent在正常执行时也会被误判——比如它连续读三个不同的文件action都是read_fileembedding相似度很高但其实是在做正常的信息收集。修复方案是把action的payload也纳入相似度计算。read_file(a.py)和read_file(b.py)的payload不同相似度就降下来了。另外我加了一个白名单read_file和think这类只读操作不参与循环检测只有edit_file和run_cmd这类有副作用的操作才检测。5.4 快照回滚把Agent的记忆也回滚了这是个隐蔽的坑。我用影子目录做文件快照回滚时文件恢复了但agent的对话历史里还记着我已经改了X文件。文件回滚了记忆没回滚两者不一致。解决方案是回滚时同步回滚对话历史。具体做法是给每个checkpoint关联一个对话历史的index回滚时把对话历史截断到对应位置。这样agent的记忆和文件状态始终一致。这个机制实现起来有点绕但它是自愈系统可靠性的基石。不一致的状态比不恢复更危险。5.5 性能开销观测不能拖慢Agent全量观测的开销比我想的大。每个step都做文件快照、跑diff、算embedding一个step额外增加200-500ms。对于需要几十步的任务累积起来就是十几秒的额外延迟。优化思路是分级观测不是每个step都做全量观测。think类step只记时间戳read_file只记文件路径只有edit_file和run_cmd才做全量快照和对账。这样开销降到了可接受的范围。另外快照用rsync --link-dest做增量第一次全量后续只存变化的部分磁盘和IO开销都大幅下降。6. 把可观测和自愈串成一个闭环单独做可观测或者单独做自愈价值都有限。真正的价值在于闭环观测信号触发自愈自愈结果反馈到观测观测数据又用于优化自愈策略。我现在的架构大概是每个step产生StepRecordStepRecord进入一个SignalExtractor提取异常信号信号触发SelfHealOrchestrator执行自愈自愈的每一步又产生新的StepRecord。整个链路用OTel串起来在Grafana里能看到完整的闭环流转。这个闭环跑顺之后最直观的变化是agent的装死率大幅下降。以前它经常说完成了但实际没完成现在这种情况会被phantom_edit检测抓到触发自愈要么真的完成要么明确报告失败。对我来说一个能诚实说我失败了的agent比一个假装成功的agent有价值得多。如果你现在正在搭agent系统我的建议是先做观测再做自愈。观测是自愈的前提没有可靠的观测信号自愈就是瞎猜。而观测里优先做预期-实际对账这是投入产出比最高的一环。至于工具选型LangFuse适合快速起步OTel适合长期建设inotifygit适合轻量场景按你的实际情况选就行不用追求一步到位。
返回列表