AI coding agent 可观测与自愈:开源思路与实操
1. 为什么 AI coding agent 需要“双眼睛”1.1 从“能写代码”到“写得靠谱”的鸿沟过去一年我陆续把几个 AI coding agent 接入了日常开发流程。最开始的新鲜感很快被现实冲淡agent 能一口气生成两百行代码但跑起来报错的时候它自己完全不知道发生了什么。它就像一个闭着眼睛写代码的人写完就交差至于代码能不能跑、跑起来有没有异常、异常出在哪一行它一概不知。这就是当前大多数 AI coding agent 的核心短板——缺乏可观测性。所谓可观测不是简单地打印几行日志而是让 agent 能够感知自己产出的代码在运行时的状态编译是否通过、测试是否绿灯、运行时有没有抛异常、性能有没有退化。没有这层感知agent 的“自主性”就是空中楼阁它只能依赖人类的反馈来修正本质上还是一个高级一点的代码补全工具。而“自愈”则是可观测的自然延伸。当 agent 能够观测到问题之后下一步就是让它自己尝试修复。这两件事合在一起我称之为给 AI coding agent 加的“双眼”一只眼睛看运行状态另一只眼睛看问题根因然后驱动它自己动手修。1.2 这套思路适合谁参考如果你正在做以下几件事这篇内容应该对你有直接帮助你在搭建或调优自己的 AI coding agent希望它不只是生成代码还能闭环验证你在做研发效能工具链想把 agent 接入 CI/CD 流程你对开源方案感兴趣想看看社区里已经有哪些可复用的组件你单纯好奇让 agent 自己修 bug 这件事到底卡在哪几个环节。我不会讲太多理论重点放在开源思路的拆解和我自己踩过的坑上。所有方案都是我实际跑过或者深度读过源码的能落地的才写进来。1.3 核心关键词速览为了方便后续展开先把几个核心概念对齐一下关键词我的理解AI coding agent能自主规划、生成、修改代码的智能体通常基于 LLM 驱动可观测agent 能获取代码运行时的编译、测试、日志、异常等信息自愈agent 基于可观测信息自主定位并修复问题开源思路社区中可复用的架构模式、工具链和实现方案这三者不是孤立的。可观测是输入自愈是输出开源思路是中间的实现路径。下面我按这个逻辑逐层拆开。2. 可观测层的开源思路拆解2.1 为什么不能只靠“读日志”很多人第一反应是可观测不就是让 agent 读日志吗我一开始也这么想后来发现远远不够。日志只是可观测的一个维度而且是最粗糙的那个。真正要让 agent 理解运行状态至少需要四类信息编译/构建结果代码能不能过编译这是第一道门槛测试执行结果单元测试、集成测试的通过率和失败详情运行时异常进程有没有崩溃、有没有未捕获的异常栈性能指标响应时间、内存占用、CPU 使用率有没有异常波动。只给 agent 看日志文本它很难从中提取结构化的问题定位信息。所以可观测层的核心任务是把非结构化的运行信息转化成 agent 能理解的结构化反馈。2.2 开源方案一基于 LSP 的静态诊断接入第一个思路是利用LSPLanguage Server Protocol做静态诊断。LSP 本来是给编辑器用的但它提供的诊断信息diagnostics对 agent 来说非常友好——结构化、带位置、带严重级别。我试过的做法是在 agent 生成代码后不急着让它跑而是先通过 LSP 客户端拉取诊断结果。比如用pyright做 Python 的类型检查用rust-analyzer做 Rust 的编译诊断。这些工具的输出本身就是 JSON 格式agent 可以直接解析。# 伪代码通过 LSP 获取诊断信息 from lsp_client import LSPClient client LSPClient(server_command[pyright-langserver, --stdio]) client.initialize() diagnostics client.get_diagnostics(file_pathgenerated_code.py) for diag in diagnostics: print(f[{diag.severity}] {diag.message} at line {diag.range.start.line})这个思路的好处是快。静态诊断不需要真正运行代码毫秒级就能返回结果。agent 可以在生成代码后的第一时间拿到反馈快速迭代。缺点是只能发现语法和类型层面的问题逻辑错误和运行时问题它管不了。注意LSP 的诊断信息在不同语言服务器之间差异很大。Python 的 pyright 和 mypy 输出格式就不一样agent 的解析逻辑需要做适配。我建议先锁定一两种主力语言不要一上来就搞多语言支持。2.3 开源方案二容器化沙箱执行 结构化回传静态诊断之后下一步就是真正跑起来。这里的关键词是沙箱。你不能让 agent 生成的代码直接在宿主机上跑万一它写了rm -rf /呢虽然概率低但风险必须隔离。我的做法是用 Docker 起一个轻量沙箱把 agent 生成的代码挂载进去执行预设的测试命令然后把结果结构化回传。开源社区里firejail、nsjail这类沙箱工具也可以考虑但 Docker 的生态最成熟调试也方便。# 沙箱执行示例 docker run --rm \ -v $(pwd)/generated:/workspace \ -w /workspace \ --network none \ --memory 512m \ --cpus 1 \ python:3.11-slim \ sh -c pip install -r requirements.txt pytest --json-report这里有几个参数值得说明--network none断网防止 agent 生成的代码偷偷下载依赖或者外传数据--memory 512m限制内存防止死循环把宿主机拖垮--cpus 1限制 CPU同样是防止资源耗尽--json-report让 pytest 输出 JSON 格式的测试报告方便 agent 解析。实测下来这套沙箱方案在大多数场景下够用。但有一个坑如果 agent 生成的代码需要访问外部服务比如数据库断网就会导致测试失败。我的处理方式是预置一个 mock 服务或者允许访问特定的内部地址。2.4 开源方案三OpenTelemetry 做运行时追踪如果 agent 要处理的是更复杂的场景比如微服务架构下的代码修改那光靠测试报告就不够了。这时候需要引入OpenTelemetry做运行时追踪。OpenTelemetry 是 CNCF 旗下的开源可观测框架支持 trace、metrics、logs 三合一。它的价值在于agent 可以通过 trace 看到一次请求完整经过了哪些服务、每个环节耗时多少、哪里抛了异常。这种粒度的信息对于定位分布式系统中的问题非常关键。我试过的集成方式是在沙箱环境里预埋 OpenTelemetry SDKagent 生成的代码如果涉及服务调用trace 数据会自动上报到一个本地的 collector。然后 agent 通过查询 collector 的 API 来获取 trace 信息。# 查询 trace 数据 import requests traces requests.get(http://localhost:4318/v1/traces).json() for span in traces[spans]: if span[status][code] ERROR: print(fError in {span[name]}: {span[status][message]})这个方案的复杂度明显高于前两个适合有一定基础设施积累的团队。如果你只是想让 agent 修修单文件的小 bug没必要上 OpenTelemetry。2.5 三种方案的对比与选型建议方案实现难度反馈速度覆盖范围适用场景LSP 静态诊断低毫秒级语法/类型单文件生成、快速迭代容器沙箱执行中秒级逻辑/运行时多文件项目、测试驱动OpenTelemetry 追踪高秒到分钟级分布式/性能微服务、复杂调用链我的建议是从 LSP 开始逐步加沙箱最后才考虑 OpenTelemetry。不要一上来就搞全套复杂度会让你怀疑人生。先把静态诊断跑通让 agent 能自己发现语法错误这一步的收益就已经很明显了。3. 自愈层的核心机制与开源实现3.1 自愈不是“自动重试”而是“定位 修复 验证”很多人把自愈理解成“报错了就重试”这是误解。无脑重试只会浪费 token而且大概率还是错。真正的自愈需要三个环节定位从可观测数据中提取出问题的根因比如“第 42 行变量未定义”修复基于根因生成修复方案比如“在第 42 行之前定义该变量”验证修复后重新跑一遍可观测流程确认问题真的解决了。这三个环节缺一不可。我见过一些实现只做了定位和修复没有验证结果 agent 改完之后引入了新问题反而更糟。3.2 开源思路基于 ReAct 的自愈循环目前社区里比较成熟的自愈架构基本都借鉴了ReActReasoning Acting模式。核心思想是让 agent 在“思考”和“行动”之间交替先分析当前状态决定下一步做什么执行动作观察结果再进入下一轮思考。我基于这个模式搭过一个简易的自愈循环伪代码如下def self_heal_loop(code, max_iterations5): for i in range(max_iterations): # 1. 可观测获取当前状态 diagnostics run_observability(code) if not diagnostics.has_errors(): return code # 没有错误退出 # 2. 思考分析问题 analysis agent.think( codecode, diagnosticsdiagnostics, prompt分析以下诊断信息定位问题根因 ) # 3. 行动生成修复 code agent.act( codecode, analysisanalysis, prompt基于分析结果修复代码 ) return code # 达到最大迭代次数返回当前版本这个循环的关键在于最大迭代次数。我设的是 5 次超过就放弃避免无限循环烧 token。实测下来大部分简单问题 1-2 轮就能解决复杂问题 3-5 轮超过 5 轮还搞不定的基本是 agent 能力边界之外的问题再跑也没用。3.3 开源思路用“错误指纹”做去重自愈循环里有一个容易被忽略的问题同一个错误反复出现。比如 agent 第一次修复没成功第二次又生成了类似的错误代码第三次还是。如果不做去重agent 会在同一个坑里反复横跳。我的做法是给每个错误生成一个“指纹”——把错误类型、错误位置、错误消息拼接后做哈希。如果连续两轮出现相同的指纹就强制 agent 换一种修复策略或者直接退出。import hashlib def error_fingerprint(diagnostic): raw f{diagnostic.type}:{diagnostic.line}:{diagnostic.message} return hashlib.md5(raw.encode()).hexdigest() seen_fingerprints set() for iteration in range(max_iterations): diagnostics run_observability(code) fingerprints {error_fingerprint(d) for d in diagnostics} if fingerprints seen_fingerprints: # 出现重复错误强制换策略 agent.force_new_strategy() seen_fingerprints | fingerprints这个小技巧看起来简单但实际效果很好。它把自愈从“盲目重试”变成了“有记忆的迭代”。3.4 开源思路人类反馈的优雅降级自愈不可能 100% 成功。当 agent 搞不定的时候怎么把问题交回给人类也是一门学问。我的原则是降级时要给出完整的上下文而不是甩一个“我失败了”。具体来说降级报告应该包含原始代码和最终代码的 diff每一轮的可观测结果agent 每一轮的思考过程agent 自己认为最可能的根因建议人类关注的几个点。这样人类接手的时候不需要从头排查直接看 agent 的分析报告就行。我实测下来这种降级方式能节省大量沟通成本。3.5 自愈能力的边界在哪里必须承认自愈不是万能的。根据我的经验以下场景 agent 基本搞不定需求层面的错误代码逻辑符合需求文档但需求文档本身是错的架构层面的问题需要重构才能解决不是改几行代码能搞定的外部依赖问题第三方库的 bug、网络问题、环境配置问题性能优化能跑但慢这种问题 agent 很难判断“多慢算慢”。认清这些边界才能合理设定自愈的预期。我的做法是自愈只处理“明确的、局部的、可验证的”问题其他一律降级给人类。4. 把可观测和自愈串起来完整实操流程4.1 整体架构设计把前面的思路串起来一个完整的 AI coding agent 可观测与自愈系统大概长这样[Agent 生成代码] ↓ [LSP 静态诊断] → 有错误 → 进入自愈循环 ↓ 无错误 [沙箱执行测试] → 有失败 → 进入自愈循环 ↓ 全部通过 [输出最终代码]自愈循环内部[获取诊断] → [分析根因] → [生成修复] → [验证修复] → 通过 → 退出 ↑ ↓ 不通过 └──────────────── 迭代最多 5 轮──────────────────┘这个架构不复杂但每个环节都有细节要注意。下面我按实操顺序拆开讲。4.2 第一步搭建 LSP 诊断通道以 Python 为例我选的是pyright作为语言服务器。安装很简单pip install pyright然后在 agent 的代码生成环节之后插入诊断调用import subprocess import json def get_pyright_diagnostics(file_path): result subprocess.run( [pyright, --outputjson, file_path], capture_outputTrue, textTrue ) data json.loads(result.stdout) return data.get(generalDiagnostics, [])这里有个坑pyright的 JSON 输出格式在不同版本之间有变化。我锁定了pyright1.1.350这个版本避免升级导致解析失败。如果你要用最新版记得先看一眼输出格式。4.3 第二步配置沙箱执行环境沙箱我用的是 Docker基础镜像选python:3.11-slim够轻量。Dockerfile 大概这样FROM python:3.11-slim RUN pip install pytest pytest-json-report WORKDIR /workspace CMD [pytest, --json-report, --json-report-file/tmp/report.json]构建和运行docker build -t agent-sandbox . docker run --rm \ -v $(pwd)/generated:/workspace \ --network none \ --memory 512m \ agent-sandbox测试报告会输出到/tmp/report.jsonagent 读取后解析def parse_test_report(report_path): with open(report_path) as f: report json.load(f) failures [] for test in report[tests]: if test[outcome] failed: failures.append({ name: test[nodeid], message: test[call][longrepr] }) return failures4.4 第三步实现自愈循环把前面两步串起来自愈循环的核心逻辑def auto_heal(initial_code, max_iterations5): code initial_code history [] for i in range(max_iterations): # 静态诊断 static_errors get_pyright_diagnostics(code) if static_errors: history.append({iteration: i, type: static, errors: static_errors}) code agent.fix(code, static_errors) continue # 沙箱执行 test_failures run_in_sandbox(code) if test_failures: history.append({iteration: i, type: test, errors: test_failures}) code agent.fix(code, test_failures) continue # 全部通过 return {status: success, code: code, history: history} # 达到最大迭代次数 return {status: failed, code: code, history: history}这段代码看起来简单但实际跑起来有几个细节要处理agent.fix()的 prompt 要包含历史错误信息让 agent 知道之前试过什么每轮迭代要记录代码快照方便回溯如果连续两轮错误相同要触发策略切换。4.5 第四步验证与回归自愈完成后不能直接信任 agent 的修复结果。我的做法是跑一遍回归测试把原始测试用例和新增的边界用例都跑一遍确认修复没有引入新问题。def regression_check(fixed_code, original_tests, new_tests): all_tests original_tests new_tests results run_in_sandbox(fixed_code, all_tests) if results.failed_count 0: return {status: regression_failed, details: results.failures} return {status: regression_passed}这一步很关键。我遇到过好几次 agent 修好了原问题但把另一个测试搞挂了。没有回归检查这种问题就会漏到生产环境。4.6 实操现场记录一次真实的自愈过程分享一个我实际跑过的案例。agent 生成了一段 Python 代码功能是解析 JSON 并提取字段。静态诊断通过但沙箱测试报错FAILED test_parse.py::test_missing_field - KeyError: name自愈循环第一轮诊断KeyError: name位置在data[name]分析agent 认为是没有做字段存在性检查修复改成data.get(name, )。第二轮测试通过。整个过程耗时约 12 秒消耗 token 约 800 个。这个案例比较简单但能说明流程是通的。另一个复杂一点的案例agent 生成的代码涉及多文件调用静态诊断通过但集成测试报错。自愈循环跑了 3 轮才搞定第一轮修错了方向第二轮引入了新错误第三轮才找到真正的根因。这说明复杂问题的自愈成本明显更高需要合理设定迭代上限。5. 常见问题与排查技巧实录5.1 诊断信息太多agent 抓不住重点现象LSP 返回几十条诊断信息agent 的 prompt 被塞满修复效果反而变差。排查思路诊断信息需要做优先级排序。我的做法是按严重级别过滤只保留error级别忽略warning和information。如果 error 还是太多就按文件分组每次只处理一个文件。def prioritize_diagnostics(diagnostics): errors [d for d in diagnostics if d[severity] error] # 按文件分组 by_file {} for d in errors: by_file.setdefault(d[file], []).append(d) return by_file5.2 沙箱执行超时agent 卡死现象agent 生成的代码里有死循环沙箱执行一直不返回。排查思路Docker 的--stop-timeout参数可以设置超时但更稳妥的做法是在测试命令层面加超时timeout 30s pytest --json-report如果 30 秒还没跑完直接杀掉把超时作为一条诊断信息返回给 agent。agent 看到“执行超时”之后通常会去检查循环条件。5.3 自愈循环反复修同一个错误现象agent 连续几轮都在修同一个问题但每次修完还是报同样的错。排查思路这就是前面提到的“错误指纹”要解决的问题。我的处理方式是连续两轮指纹相同就在 prompt 里明确告诉 agent“你之前的修复方案无效请换一种思路”。实测下来这个提示能显著提高跳出循环的概率。5.4 修复引入了新问题现象原问题修好了但回归测试挂了。排查思路这是自愈系统最危险的情况。我的做法是强制回归检查任何修复都必须通过全部原有测试才能算成功。如果回归失败就把新失败的测试也加入诊断信息让 agent 在下一轮一起修。5.5 常见问题速查表问题可能原因解决方向诊断信息过多未做优先级过滤按严重级别和文件分组沙箱执行超时代码有死循环加 timeout超时作为诊断返回反复修同一错误缺少错误指纹去重连续相同指纹时强制换策略修复引入新问题缺少回归检查强制跑全量测试agent 修复方向错误prompt 缺少上下文把历史错误和尝试记录塞进 prompttoken 消耗过大迭代次数过多设 max_iterations超限降级5.6 几个我踩过的坑坑一不要信任 agent 的“自我验证”。我一开始让 agent 自己判断修复是否成功结果它经常误判。后来改成用沙箱测试结果做客观验证准确率大幅提升。坑二沙箱镜像要预装依赖。如果每次执行都pip install速度会很慢。我的做法是提前把常用依赖打进镜像执行时只挂载代码。坑三诊断信息的格式要统一。LSP 和测试报告的输出格式不一样agent 解析逻辑要分开写。我后来定义了一个统一的Diagnostic数据结构把两种来源都转成同一个格式agent 处理起来就简单了。坑四不要忽略“无错误”的情况。有时候 agent 生成的代码没有报错但逻辑是错的。这种情况可观测层发现不了需要靠更上层的需求验证。我的做法是在 prompt 里加入需求描述让 agent 自己对照检查。6. 开源生态里还有哪些可复用的组件6.1 值得关注的几个开源项目在搭建这套系统的过程中我参考了不少开源项目。这里列几个我觉得有价值的SWE-agent普林斯顿开源的 agent 框架核心思路是让 agent 在真实代码库上做修复它的可观测层设计很值得借鉴OpenHands前身是 OpenDevin支持沙箱执行和自愈循环代码结构清晰Aider虽然是命令行工具但它的 diff 生成和测试反馈机制很成熟ContinueIDE 插件形态但它的 LSP 集成思路可以直接复用。这些项目不需要全盘照搬挑其中一两个模块深入研究就行。我个人的经验是看源码比看文档收获大尤其是它们处理边界情况的方式。6.2 开源方案的选型原则选开源方案的时候我一般看三点可调试性出问题能不能快速定位日志和诊断信息是否充分可替换性核心组件能不能单独替换比如沙箱从 Docker 换成别的社区活跃度issue 响应速度和 PR 合并频率这决定了长期维护成本。这三点比功能列表更重要。我见过太多功能强大但没人维护的项目用起来就是给自己挖坑。6.3 后续可以扩展的方向这套系统跑通之后还有几个方向可以继续深挖多语言支持目前主要跑 Python后续可以扩展到 TypeScript、Go性能回归检测不只是看测试通过率还要看执行时间有没有退化自愈策略学习把历史修复案例存下来让 agent 参考类似问题的修复方案人类反馈闭环把人类手动修复的案例反哺给 agent持续优化。这些方向我还在探索中有进展再分享。目前这套基础版本已经能覆盖大部分日常场景稳定性也经过了一段时间的验证。我个人在实际操作中的体会是可观测和自愈不是两个独立的功能而是一个闭环的两端。可观测做得越细自愈的成功率越高自愈的反馈越结构化可观测层的设计就越有针对性。两者互相成就缺一不可。如果你刚开始搭这套系统建议先把 LSP 诊断跑通这一步的投入产出比最高而且能为后续的沙箱和自愈打下基础。