让 AI Agent 系统自己发现 bug、自己提修复 PR:自我进化的 Harness

作者:谭sir日期:2026/6/29

本文介绍怎么让 AI Agent 的工程代码(Harness)具备自我进化能力——自动记录运行数据、自动识别错误模式、自动生成修复 PR(Pull Request,合并请求)。内容覆盖监控、错误模式识别、自动修复、行为分析和生产落地方案,每一章都会配合 demo 项目 evo-agent-demo 的代码和运行结果来讲解。

从一个 bug 说起

假设你做了一个 AI Agent 产品,它可以搜索资料、查数据库、执行代码。上线前也在内部进行了反复测试,并且没发现什么问题,于是就正式上线了。

但产品上线一段时间后,开始陆续的收到用户反馈,产品产生了各种各样的错误。因为内部测试覆盖不到所有的使用场景,而真实用户的数量远超团队测试人员,并且他们的提问方式、使用习惯、边界操作远远超出了测试用例的范围,导致出现了很多意料之外的情况。比如客服收到投诉说“问了个问题,AI 直接报错了”,技术论坛上有人发帖说多轮对话后程序崩了,运营那边也反馈搜索功能时好时坏。

这种靠用户反馈发现问题的方式很被动——等用户反馈时,往往已经有很多用户受到了影响。对开发来说更麻烦的是:光凭一句反馈很难定位到 bug 的真正原因,因为手上没有出问题时的执行上下文。

大多数 AI Agent 产品的 bug 基本上都出现在包裹模型的那层工程代码上,这层代码有一个专门的名字:Harness。Harness(意思是马具、马鞍) 就是驱动模型运转的工程系统。打个比方:模型是 CPU,Harness 是操作系统——操作系统管理进程调度和内存分配,Harness 管理工具调用、上下文窗口和错误恢复。

从被动到主动

既然问题出在 Harness 上,那么我们得知道 Harness 每次执行的时候到底发生了什么。我们可以给 Agent 加一层 Tracing,这相当于给 Agent 装上行车记录仪。Agent 每次执行的时候,调用了什么工具、传的参数是什么、响应结果是什么等等,都会记到数据库里。这样就不用等用户来反馈,我们自己查数据库就能看到哪些请求出了问题。

但每天产生的请求这么多,可能有几千上万条,其中的错误也不少,依赖人工逐条查看不太现实。所以还需要让系统自动按类型把错误分组(比如所有 429 限流错误归到一组),然后判断每一类错误是用户的问题/供应商的问题、还是 Harness 自身的 bug——如果是 Harness 的 bug,需要让 LLM 根据错误原因去分析相关的源码并且生成修复代码,最后提交 PR 来修复这个问题。本文后面的内容将按这个顺序来展开,并配以 evo-agent-demo 的代码和运行结果来讲解。

demo 快速体验

我们先运行一次 evo-agent-demo 来看看效果,后面会逐步拆解每个环节的实现:

1git clone https://github.com/woai3c/evo-agent-demo
2cd evo-agent-demo && pnpm install
3cp .env.example .env          # 填入至少一个 LLM API Key
4pnpm db:seed                  # 填充演示数据
5pnpm simulate --mock          # 模拟 100 条用户请求(含各种错误,不调 API)
6pnpm dev                      # 启动前后端开发服务器
7

项目启动后,打开 http://localhost:5173/admin/inspections 页面,点击“巡检:识别 Pattern”按钮,即可观察到日志面板实时输出巡检进度。巡检完成后可以切换到概览页查看各项指标的变化。

快速了解 AI Agent Harness 工程全貌

已有 AI Agent 开发经验的读者可以跳过本节,直接从“Tracing”开始。

这一节将介绍一个 Agent 产品由哪些部件组成、各自负责什么,让还没接触过 AI Agent 开发的读者先有个整体概念。

什么是 AI Agent

简单来说,AI Agent 就是能调用工具执行任务的 LLM 应用。普通聊天机器人只能回复文本,但 Agent 还可以执行搜索、查询数据库、修改代码等任务。聊天机器人的流程是“用户说话 → 模型回复 → 结束”,Agent 则是“用户说话 → 模型决定调什么工具 → 拿到结果 → 继续思考 → 可能再调工具 → … → 最终回复用户”。这个循环过程叫 Agent Loop

用本文的 demo(代号 Evo)举个例子。用户问“帮我研究一下 Hono 框架”,Agent 会先调用 webSearch 工具搜索资料,然后从结果里选几个 URL,再调用 webFetch 工具抓取页面内容,最后再综合信息写一份总结。整个过程中模型自主决定工具的选择和调用顺序,不需要人工干预。

用时序图拆解一下内部流程:

1sequenceDiagram
2    autonumber
3    actor U as 用户
4    participant AL as Agent Loop
5    participant LLM as LLM API
6    participant T as 工具
7
8    U->>AL: "帮我研究一下 Hono 框架"
9    AL->>LLM: 发送消息 + 工具列表
10    LLM-->>AL: tool_call: webSearch("Hono framework 2025")
11    AL->>T: 执行 webSearch
12    T-->>AL: 返回搜索结果
13    AL->>LLM: 将工具结果回传
14    LLM-->>AL: tool_call: webFetch("https://hono.dev")
15    AL->>T: 执行 webFetch
16    T-->>AL: 返回页面内容
17    AL->>LLM: 将工具结果回传
18    LLM-->>AL: 生成文本回复(流式输出)
19    AL->>U: 流式展示回复
20

Agent Loop 就是用户、模型和工具之间的调度中枢。用户的消息交给模型,模型决定调用什么工具,Agent Loop 执行工具并把结果交回给模型,模型再决定下一步是继续调用工具还是直接回复用户。这个循环可能转一次,也可能转十次,取决于任务的复杂度。

Agent 的核心组成

一个 Agent 产品大概由这几个部分组成:

1┌─────────────────────────────────────┐
2              Harness                
3                                     
4  ┌───────────┐  ┌───────────────┐  
5   Agent Loop│   工具系统         
6   (调度器)     (搜索/数据库/   
7                 代码执行/...)   
8  └─────┬─────┘  └───────┬───────┘  
9                                  
10  ┌─────┴────────────────┴───────┐  
11       上下文管理 (记忆)           
12  └──────────────────────────────┘  
13                                     
14  ┌──────────────────────────────┐  
15       Tracing (行车记录仪)       
16  └──────────────────────────────┘  
17                                     
18  ┌──────────────────────────────┐  
19       进化引擎 (自我完善)        
20  └──────────────────────────────┘  
21└─────────────────────────────────────┘
22         
23    ┌─────────┐
24       LLM      可替换(DeepSeek / OpenAI / Anthropic / ...)
25    └─────────┘
26

Harness 就是包裹模型的所有工程代码。模型提供推理能力,Harness 负责把这种能力变成一个能用的产品。

Agent Loop 是调度器,驱动“问模型 → 调用工具 → 再问模型”的循环。demo 里这个循环的入口是 agentLoop() 函数(packages/server/src/agent/loop.ts),通过 Vercel AI SDK 向模型发送流式请求,逐步消费模型返回的事件。模型要求调用工具时,SDK 自动执行对应的工具并把结果传回。同时为了防止无限循环,在本 demo 里单次对话最多执行 15 步(生产环境里远远不止 15 轮)。

工具系统定义了 Agent 能做什么。在 Evo demo 里有 6 个工具:网络搜索、抓取网页、读取文件、执行代码、查询数据库、发送邮件。每个工具由三部分组成:一段给模型看的自然语言描述(模型根据描述决定要不要调这个工具),一组用 Zod Schema 定义的参数格式,以及一个实际执行操作的函数。

上下文管理解决的是 token 限制问题。Agent 每调用一次工具,工具的输入和输出都要放进对话历史发给模型。10 轮对话加几次网页搜索,上下文窗口(Context Window)可能就快满了。demo 里用两个办法应对:消息压缩(packages/server/src/context/compression.ts)——对话历史的 token 数超过窗口容量的 70% 时,自动把老消息压缩成摘要;工具结果截断(packages/server/src/context/truncation.ts)——搜索返回的网页可能有几十 KB,只保留前 30,000 字符加尾部片段。

Tracing 记录每一步的执行数据——调用了什么工具、花了多少 token、耗时多久、成功没有。这些数据是后面所有分析的基础,下一节详细讲。

进化引擎从 Tracing 数据里自动发现错误、分类原因、生成修复代码。后面会分别讲它的三个部分:错误 Pattern(模式) 识别、自动修复、行为分析。

demo 项目速览

Evo 是一个 pnpm monorepo(单仓多包),包含三个包:

  • packages/shared@evo/shared)——TypeScript 类型和常量,被 server 和 web 共同引用
  • packages/server@evo/server)——Hono + Node.js 后端,包含 Agent Loop、工具系统、Tracing、进化引擎、REST API
  • packages/web@evo/web)——Vite + React 19 + Tailwind + shadcn/ui 前端,包含聊天界面和管理后台

数据库用 better-sqlite3,LLM 接入用 Vercel AI SDK,支持 DeepSeek、OpenAI、Anthropic、阿里百炼、智谱、Moonshot 六家供应商,默认使用 DeepSeek。

关键代码位置:packages/server/src/agent/loop.ts 是 Agent Loop 的入口,packages/server/src/evolution/ 是进化引擎的全部实现。读者在阅读后续内容时,可以对照这些文件查看完整代码。

demo 与生产环境的差异

demo 做了不少简化,和生产环境的主要差异:

维度demo 实现生产环境替代方案
巡检触发管理后台手动点击“开始巡检”定时任务(每 1-2 小时),分析上一周期的新增错误
自动修复触发管理后台手动点击“自动修复”定时任务(每天凌晨),对未修复的 harness_bug 生成 PR
数据库SQLite 单文件,适合本地开发PostgreSQL / MySQL,支持并发和多实例
模拟数据pnpm simulate --mock 批量注入真实用户流量产生的 trace 数据
错误注入simulate-errors.ts 随机注入错误生产环境中自然发生的错误
PR 提交提交到本地 Git 仓库通过 GitHub/GitLab API 提交到代码仓库,走 CI/CD(持续集成/持续部署)流程

demo 里所有操作都是手动触发的——点击按钮执行一次巡检、点击按钮生成修复 PR。这是为了让你能一步步看到每个环节的输入和输出。生产环境中这些操作应该由定时任务自动执行,调用的函数和 demo 里手动触发的是同一个。

Tracing:给 Agent 装上“行车记录仪”

行车记录仪的作用是出了事故看录像,没出事也能复盘驾驶习惯——Tracing 在 Agent 系统里的作用差不多。不过和普通日志不同,Tracing 记录的是结构化的数据,而且这些数据会被后面的自动分析程序持续消费,不只是出问题了才翻看。

Trace 数据结构

举个例子。demo 内置了一个音乐库作为示例数据,假设用户问“哪个歌手专辑最多”,Tracing 会把 Agent 的整个执行过程记录下来:

1Operation: op_abc123
2  ├─ Step 0: call_llm   模型决定调 dbQuery     (耗时 800ms, tokens: 1200)
3  ├─ Step 1: call_tool  dbQuery 执行 SQL        (耗时 50ms, 成功)
4  ├─ Step 2: call_llm   模型根据查询结果组织回答 (耗时 600ms, tokens: 900)
5  └─ 结果: success, 总耗时 1450ms
6

整个过程被拆成一个 Operation(操作)和若干 Step(步骤)。Operation 对应一次完整的用户请求,记录总耗时、token 用量、最终状态这些汇总信息。每个 Step 对应 Agent Loop 里的一次 LLM 调用或一次工具调用。

每个 Step 记录以下字段:

字段说明
type步骤类型:call_llm(调用模型)或 call_tool(调用工具)
duration_ms这一步的耗时(毫秒)
tokens消耗的 token 数(仅 call_llm 步骤),包含 input、output、cached 三个分量
tool_name调用的工具名称(仅 call_tool 步骤)
tool_success工具调用是否成功(布尔值)
tool_input工具调用的输入参数
tool_output工具返回的完整结果
error如果出错,错误的详细信息
llm_response模型的完整文本回复(仅 call_llm 步骤)

tool_inputtool_output 保留了工具调用的完整上下文,管理面板里可以查看每一步的输入输出,方便排查问题。llm_response 保留了模型回复,后续重放引擎需要这些数据来复现执行过程。

Tracing 的接入方式

源码:packages/server/src/agent/loop.ts

demo 里 Agent Loop 每执行完一步就记录一条 trace 数据:工具调用完成后,执行一次 tracer.onToolResult() 记录调用结果;LLM 回复完成后,执行 tracer.onStepFinish() 记录 token 用量。这样执行代码和记录代码始终成对出现,不会出现某一步执行了但漏掉记录的情况。

trace 调用在 Agent Loop 中的位置:

1flowchart TD
2    A[用户消息到达] --> B[创建 Tracer 实例]
3    B --> C[调用 streamText 开始循环]
4    C --> D{读取 fullStream 事件}
5    D -->|step-start| E["tracer.onStepStart()"]
6    E --> D
7    D -->|tool-call| F["tracer.onToolCallStart()"]
8    F --> D
9    D -->|tool-result| G["tracer.onToolResult(toolName, input, success, ...)"]
10    G --> D
11    D -->|step-finish| H["tracer.onStepFinish(tokenUsage, context, llmResponse)"]
12    H --> D
13    D -->|error| I["tracer.onError(message)"]
14    I --> J[返回 error 事件]
15    D -->|流结束| K["tracer.finish(status)"]
16    K --> L[返回 done 事件]
17

简化后的代码(完整实现见源码):

1export async function* agentLoop(params: AgentLoopParams): AsyncGenerator<StreamEvent> {
2  const tracer = new Tracer({ userId, conversationId, provider, model: modelId })
3
4  const result = streamText({ model: llmModel, system: SYSTEM_PROMPT, messages, tools, maxSteps: 15 })
5
6  for await (const part of result.fullStream) {
7    switch (part.type) {
8      case 'step-start':
9        tracer.onStepStart()
10        break
11      case 'tool-call':
12        tracer.onToolCallStart()
13        yield { type: 'tool-call', toolName: part.toolName, input: part.args }
14        break
15      case 'tool-result':
16        tracer.onToolResult(part.toolName, toolInput, success, outputSize, resultObj, errorMsg)
17        yield { type: 'tool-result', toolName: part.toolName, success, outputSize }
18        break
19      case 'step-finish':
20        tracer.onStepFinish(tokenUsage, contextInfo, stepText)
21        break
22      case 'error':
23        tracer.onError(String(part.error))
24        yield { type: 'error', message: String(part.error) }
25        break
26    }
27  }
28
29  const operationId = tracer.finish(status)
30  yield { type: 'done', operationId }
31}
32

每个 case 分支里都有一行 tracer.onXxx() 调用。Tracer 在函数入口创建,在出口 finish()。Agent Loop 跑了多少步,trace 就记了多少步。

demo 里的 Tracer 是自己实现的,因为后面的进化引擎需要用 SQL 查询 trace 数据(分桶、匹配、回扫)。如果你的产品已经接入了 Sentry、PostHog 等平台,可以在 Agent Loop 的相同位置用这些平台的 API 做手动埋点,记录的数据字段跟上面表格里的一样。但埋点数据最终要同步到自己的数据库里,或者通过平台 API 导出,因为进化引擎需要直接查询这些数据。

两个设计要点:即时写入与 token 记录

即时写入。Agent 每执行完一步,Tracer 立刻把这一步的数据写入 SQLite,不在内存中缓存。因为崩溃时的数据是最有价值的,假设 Agent 在第 5 步执行崩溃,但前 4 步的记录已经在数据库里了,所以分析崩溃原因时有上下文可查。如果缓存在内存中批量写入,程序崩溃整条 trace 数据就丢了。demo 里 Tracer 的每个 onXxx() 方法都会立刻调用 traceStore.insertStep() 写入数据库。SQLite 开启 WAL(Write-Ahead Logging)模式后,每步写一次不会有性能问题。

token 记录。Tracer 在每个 step-finish 事件里记录 promptTokenscompletionTokens 和缓存命中数(不同供应商字段名不一样——DeepSeek 叫 promptCacheHitTokens,Anthropic 叫 cacheReadInputTokens,OpenAI 叫 cachedPromptTokens)。Operation 结束时 Tracer.finish() 汇总所有 step 的 token 用量。后面行为分析会用到这些数据,比如发现“某类操作的 token 消耗是其他操作的 10 倍”。

错误 Pattern 自动识别

Trace 能告诉我们“出了什么错”,但不能告诉我们“该怎么处理”。这一节讲系统怎么自动把错误分类,以及分类规则(Pattern)是怎么生成的。

Trace 里的错误记录

Trace 记录的每条错误包含供应商、错误类型、HTTP 状态码、工具名、错误消息这几个字段。下面是几种典型错误:

搜索 API 限流:

1provider=deepseek, error_type=rate_limit, status_code=429, tool_name=null
2message: "429 Too Many Requests"
3

SQL 查询了不存在的表:

1provider=null, error_type=tool_error, status_code=null, tool_name=dbQuery
2message: "no such table: orders"
3

模型返回了无法解析的 JSON:

1provider=openai, error_type=invalid_json, status_code=null, tool_name=null
2message: "Unexpected token at position 1234"
3

上下文超出 token 限制:

1provider=deepseek, error_type=context_overflow, status_code=400, tool_name=null
2message: "This model's maximum context length is 65536 tokens"
3

这些错误数据都带着供应商、类型、状态码这些字段,但字段本身只描述了“发生了什么”,没有回答“该怎么处理”。所以我们需要一条规则来判断:这种错误属于哪个类别?这就是 Pattern 的作用。

Pattern:错误的分类规则

源码:packages/server/src/evolution/pattern-matcher.ts

Pattern 是存在数据库里的一条错误分类规则。每条 Pattern 由三部分组成:名称、匹配条件(哪些字段要满足什么值)、分类(这种错误属于哪个类别)。当一条错误的字段和某个 Pattern 的匹配条件吻合时,系统就知道这条错误属于哪一类,不需要人工判断。比如:

1Pattern 示例:
2  名称:deepseek-rate-limit-429
3  分类:provider_error(供应商的问题,不是 Harness 能修的)
4  匹配规则:provider = deepseek AND statusCode = 429
5

不同的错误分类对应四种处理方式:user_error(用户的问题,比如 API Key 过期)不用改代码;provider_error(供应商的问题,比如限流)不用改代码,但可能要加重试;harness_bug(我们自己的 bug)需要修代码——后面“自动修复”那节处理的就是这一类;ignore(管理员认为不需要处理的)直接跳过,自动修复时也会忽略。

每条错误写入数据库时,系统会调 matchError() 尝试匹配现有 Pattern——逐一对比 provider、errorType、statusCode、toolName 是否符合条件,最后用正则检查错误消息,全部匹配上才算命中。匹配不上的错误留在那里,等下一轮巡检时被分桶、交给 LLM 分析、生成新 Pattern。

1export function matchError(error: ErrorToMatch): Pattern | null {
2  const patterns = loadPatterns()
3  for (const pattern of patterns) {
4    const rule = pattern.matchRule
5    if (rule.provider && rule.provider !== '*' && rule.provider !== error.provider) continue
6    if (rule.errorType && rule.errorType !== error.errorType) continue
7    if (rule.statusCode != null && rule.statusCode !== error.statusCode) continue
8    if (rule.toolName && rule.toolName !== error.toolName) continue
9    if (rule.messageRegex) {
10      if (!new RegExp(rule.messageRegex, 'i').test(error.message)) continue
11    }
12    // 命中:更新计数
13    return pattern
14  }
15  return null
16}
17

LLM 巡检:生成新 Pattern

源码:packages/server/src/evolution/error-bucketer.tspackages/server/src/evolution/inspector.ts

上一节说到,匹配不上的错误会留在那里等巡检处理。系统刚上线时 Pattern 库是空的,所有的错误都是未匹配的。巡检就是用 LLM 来分析这些错误、生成新 Pattern 的过程。

分桶

LLM 不需要逐条看错误。同一种错误出现 15 次和出现 1 次,根因都是一样的。所以巡检先把未匹配的错误按类型归组——bucketErrors() 按供应商、错误类型、HTTP 状态码、工具名、错误消息五个维度做 GROUP BY 分组,并返回每个桶的错误条数:

1const rows = db.prepare(
2  `SELECT e.provider, e.error_type, e.status_code, e.tool_name, e.message, COUNT(*) as count
3   FROM errors e ${where}
4   GROUP BY e.provider, e.error_type, e.status_code, e.tool_name, e.message
5   ORDER BY count DESC`
6).all(...params)
7

分桶结果示例:

数量
deepseek × rate_limit × 429 × null15
null × tool_error × null × dbQuery8
openai × invalid_json × null × null3

比如表里的 15 条 deepseek 限流错误,分桶后就是一个桶,LLM 只需要分析一次。bucketErrors()unmatched 参数只返回还没被 Pattern 覆盖的错误桶,已经有 Pattern 的不需要再分析。

LLM 分析

分桶之后,系统把未匹配的错误桶的汇总信息——供应商、错误类型、状态码、工具名、错误消息、出现次数——发给 LLM,让它判断每个桶的根因并生成对应的 Pattern。

LLM 返回的结果要直接写入数据库,所以格式不能随意。这里用 Zod 定义一个 Schema,通过 Vercel AI SDK 的 generateObject 传给模型,模型必须按这个结构返回 JSON,格式不对时 SDK 会自动重试:

1const PatternSuggestionSchema = z.object({
2  patterns: z.array(z.object({
3    name: z.string().describe('Human-readable pattern name, e.g. deepseek-rate-limit-429'),
4    category: z.enum(['user_error', 'provider_error', 'harness_bug', 'ignore']),
5    errorType: z.string(),
6    matchRule: z.object({
7      statusCode: z.number().nullable().optional(),
8      provider: z.string().optional(),
9      toolName: z.string().nullable().optional(),
10      messageRegex: z.string().optional(),
11    }),
12    reasoning: z.string().describe('Why you classified it this way'),
13  })),
14  bugs: z.array(z.object({
15    title: z.string(),
16    description: z.string(),
17    rootCause: z.string(),
18    suggestedFix: z.string(),
19    severity: z.enum(['low', 'medium', 'high']),
20  })),
21  summary: z.string(),
22})
23
24const result = await generateObject({
25  model,
26  schema: PatternSuggestionSchema,
27  prompt,  // 包含未匹配错误桶的详细信息
28  abortSignal: AbortSignal.timeout(120_000),
29})
30

上面代码中的 patterns 字段是 LLM 为每个错误桶生成的 Pattern——名称、分类、匹配规则和分类理由。而 bugs 字段是 harness_bug 的详细分析:根因、修复建议、严重程度——后面“自动修复”那节会用到这些信息。summary 字段是本轮巡检的总结。

LLM 返回结果后,applyFixes() 把新 Pattern 写入数据库,并立刻回扫(Backfill)所有历史错误——找到所有 pattern_id 为空的记录,用新 Pattern 逐一对比,能匹配上的就标记。之前没被识别的错误,现在有了 Pattern 之后就自动归类了。

1flowchart TD
2    A[未匹配的错误桶] --> B[构造 prompt]
3    B --> C[&#34;generateObject() + Zod Schema&#34;]
4    C --> D[类型安全的结构化结果]
5    D --> E[新 Pattern 写入数据库]
6    E --> F[回扫历史错误]
7    F --> G[之前未匹配的错误被自动归类]
8

读者可以用 demo 自己验证。执行 pnpm simulate --mock 注入模拟请求后,在管理面板里手动执行一次巡检。巡检会取出所有未匹配的错误桶,LLM 为每个桶生成 Pattern,然后用新 Pattern 回扫历史错误。巡检执行完毕后,之前积累的未匹配错误基本都能被覆盖。

自动修复:让系统自己提 PR

上一节识别出了 harness_bug。这一节让系统自动生成修复代码、提交 PR。从定位文件到创建 PR,中间不需要开发者参与,但合并 PR 还是要开发者审核。

修复流程

源码:packages/server/src/evolution/auto-pr.ts

自动修复用的是和聊天一样的 Agent Loop 架构——LLM 通过工具自主搜索文件、读取代码、修改代码。

上一节的巡检会把 Harness 自身的 bug 标记为 harness_bug Pattern,并将状态更新为 fix_status = 'unfixed'。自动修复会逐个处理这些未修复的 Pattern,为每个 Pattern 生成修复代码并提交 PR。除了 harness_bug,被标为 critical 的不健康行为(下一节讲)也会一并处理。bug 修复的分支用 fix/ 前缀,行为优化用 improve/

对每个待修复目标,系统先创建一个 git 分支,然后启动一个修复 Agent。这个 Agent 配备了文件搜索(globgrep)、读写(readFileeditFilewriteFile)和提交(submitFix)等工具。Agent 拿到错误信息后,自己搜索相关文件、读取代码、定位问题、应用修改——和聊天 Agent 一样,都是 LLM 通过工具自主完成的。

Agent 完成修改后,系统会提交所有修改过的文件。如果已经有远程仓库,会自动 push 并创建 PR;否则只在本地建分支。最后更新 fix_status 状态为 pr_created

下面是一些关于自动修复运行过程的截图以及自动修复提交的 PR 截图:

安全边界:为什么不让它自己合并代码

自动修复只生成 PR,合并仍然需要开发者审核。LLM 生成的代码可能引入新的问题,在没有自动化测试覆盖修改行为的情况下,让系统自己合并代码风险太大。如果想跳过开发者审核,至少需要两样东西:一套覆盖了修改行为的自动化测试,以及一个能复现触发 bug 的操作的重放引擎。后面“落地指南”那节会讲重放引擎的设计。

行为分析:没报错 ≠ 没问题

前面几节解决的都是“出了什么错”。这一节看另一类问题——“没报错,但不太对劲”。

错误模式 vs 行为模式

前面几节分析的是实际报错的记录——错误 Pattern。行为分析的对象不同,它看所有操作,包括成功的。两者的区别:

错误模式行为模式
分析对象实际报错的记录所有操作,包括成功的
发现什么系统崩溃或异常没崩但效率低、质量差
举例搜索 API 429 限流、SQL 语法错误简单问题调用了 8 次工具、搜索类操作 token 消耗过高
分类方式四个固定类别(user_error / provider_error / harness_bug / ignore)LLM 按用户意图动态分组
最终动作harness_bug → 自动生成修复 PR生成改进建议,严重的也自动生成修复 PR

什么算“不健康”

用户问了个简单问题(“介绍一下你自己”),Agent 却调用了 3 次工具、花了 15 秒才回答。状态是“成功”,但效率明显不对。可能是 system prompt 对工具使用的引导不够精确,不需要工具的时候也去搜索了。

数据库查询类操作的成功率只有 65%。Agent 工具失败后会换种方式回答,所以不会报错,但三分之一的查询结果是不正确的。可能是 dbQuery 工具的 Schema 描述里缺少表结构信息,模型写出了错误的 SQL。

搜索研究类操作的平均 token 消耗是其他操作的 10 倍。要么是 webFetch 返回的页面没做截断,要么是 Agent 搜完之后抓了太多不相关的页面。

分析流程

源码:packages/server/src/evolution/behavior-analyzer.ts

要发现上面这类问题,需要先对操作数据做聚类分析。系统从数据库加载最近 100 条 operation(一条 operation 对应一次完整的用户请求,包含多个 step,前面 Tracing 章节介绍过)的摘要,把每条格式化成一行文本,全部拼进 prompt 发给 LLM:

1[0] “帮我研究一下 Hono 框架”  tools: webSearch→webFetch→webFetch  success (5 steps, 12.3s)
2[1] “哪个歌手专辑最多”  tools: dbQuery  success (3 steps, 2.1s)
3[2] “总结一下这篇文章”  tools: webFetch  success (3 steps, 4.5s)
4[3] “查一下销量前10的专辑”  tools: dbQuery  error (4 steps, 3.2s)
5...
6

每行文本包含用户消息、工具调用链、状态、步数和耗时。LLM 根据用户意图和工具使用模式对这些 operation 做分组,并返回每组的名称、描述、代表性工具序列,以及属于该组的编号列表。比如上面的 [0][2] 都涉及网络搜索和内容抓取,会被归为“网络调研 + 总结”;[1][3] 都只用了 dbQuery,会被归为“数据库查询”。分组没有预定义的类别,LLM 根据实际数据自行决定。输出格式用 generateObject + Zod Schema 约束。

demo 里取 100 条是因为所有摘要要一次性放进 prompt,太多会超出上下文窗口。生产环境应该改成按时间窗口增量分析。为了避免重复分析,系统会记录上一轮的 operation 集合签名,没有新增 operation 就跳过。

分组之后是打分。不同类型的任务性能差异很大——涉及 webSearch/webFetch 的网络调研天然比本地的 dbQuery 慢得多,用同一套阈值对两者打分不合理。所以系统根据行为组的工具序列自动选择对应的阈值:

维度轻量任务(本地工具)重度任务(含网络工具)
单次平均耗时≤ 15 秒≤ 60 秒
单次平均步数≤ 10≤ 20
单次平均 token≤ 50k≤ 150k

成功率(≥ 80%)和工具错误率(≤ 20%)不区分任务类型,这两个指标跟复杂度无关。每个维度 0.2 分,满分 1.0。

1const HEALTH_THRESHOLDS = {
2  minSuccessRate: 0.8,
3  maxToolErrorRate: 0.2,
4  light: { maxAvgDuration: 15_000, maxAvgSteps: 10, maxAvgTokens: 50_000 },
5  heavy: { maxAvgDuration: 60_000, maxAvgSteps: 20, maxAvgTokens: 150_000 },
6}
7
8const WEB_TOOLS = ['webSearch', 'webFetch']
9
10function getThresholdTier(toolSequence: string) {
11  return WEB_TOOLS.some(t => toolSequence.includes(t))
12    ? HEALTH_THRESHOLDS.heavy : HEALTH_THRESHOLDS.light
13}
14

判断逻辑很简单:看行为组的代表性工具序列里有没有网络工具,有的话用 heavy 标准,没有用 light。如果你的 Agent 有更多类型的工具,加新的分类就行。

一个行为组的健康得分低于 0.8(即至少两个维度未达标),或者触发了 low_success_ratehigh_tool_error_rate 这两个关键标记,就被判定为”不健康”。

最后是生成建议。对得分低的组,再次调用 LLM,让它给出具体的 Harness 层改进建议。LLM 收到的信息包括行为名称、具体的健康指标数值、工具调用序列,以及哪些维度不达标。要求建议针对 Harness 代码本身(比如“给 webFetch 加重试”、“优化 dbQuery 的 Schema 描述”),而不是 prompt 层面的调整。

LLM 给出的每条建议会标注严重程度:critical 表示有明确的代码修改方案,可以直接进入自动修复流程;suggestion 表示建议性改进,由开发者决定是否实施。critical 级别的建议会被标记为 fix_status = 'unfixed',后面会进入自动修复的处理队列。

落地指南:从 demo 到生产,以及用重放引擎验证修复

前面几节把 demo 里的每个模块都讲了一遍。这一节聊怎么把这些东西迁移到真实产品里,以及当前最大的短板——怎么自动验证修复到底有没有用。

迁移到你的项目

虽然 demo 用的是 TypeScript + Node.js,但实现思路跟语言没有关系:

步骤做什么说明
1在 Agent Loop 里加 TracingAgent 每执行完一步就立刻写入数据库。关键是即时写入——崩溃了也不会丢失已完成步骤的记录
2错误分桶按 provider × 错误类型 × 状态码 × 工具名 × 错误消息做 GROUP BY 分组,通过 SQL 可以看到出现次数最多的错误类型
3定时 LLM 巡检定时把未匹配的错误桶发给 LLM,通过 generateObject + Zod Schema(或对应语言的 JSON Schema 库)约束输出格式,自动生成新 Pattern
4自动回扫新 Pattern 生成后立刻回扫历史错误,之前没匹配上的错误现在能被自动归类
5自动修复巡检识别出的 harness_bug 和行为分析标记为 critical 的问题,都会进入自动修复队列。修复时 Agent 会读取相关源码、生成修复代码、提交 PR

最重要的是第 1 步。没有 trace 数据,后面什么都干不了。

调度策略

demo 里所有操作都要手动点按钮,生产环境应该配定时任务:

任务demo 中生产环境建议理由
错误巡检(识别 Pattern)手动点击每 1-2 小时自动执行错误对用户体验影响大,需要快速发现。每次只分析上一个周期内新产生的未匹配错误
行为分析(健康度评估)手动点击每 24 小时自动执行行为模式是慢变量,需要积累足够多的新操作数据才有意义
自动修复(生成 PR)手动点击每天凌晨固定时间执行一次代码修改需要审慎,安排在低峰期执行

执行顺序有依赖:巡检先执行 → 行为分析再执行 → 最后自动修复处理所有待修的。实现方式不限,cron job、GitHub Actions、Temporal 都行,核心就是定时调用同样的函数。

成本参考

按生产环境估算:每小时巡检一次,每天行为分析和自动修复各一次。

任务单轮 token 消耗月度频次月度 token
巡检~1 万(错误桶摘要 + LLM 分析)24 次/天 × 30 天 = 720 次~720 万
行为分析~2 万(100 条 operation 摘要 + 聚类)1 次/天 × 30 天 = 30 次~60 万
自动修复20-100 万/目标(完整 Agent Loop,含多轮文件读取和代码生成)~10 个目标/月~200-1000 万

自动修复消耗最大,因为修复 Agent 每次 LLM 调用都带着完整对话历史,读的文件越多、改的地方越多,上下文就越大。简单修复(改一两个文件)约 20 万 token,复杂修复(跨多个文件、需要多轮迭代)可能超过 100 万 token。

以 DeepSeek V4-Pro(输入 ¥3/百万 token,缓存命中 ¥0.025/百万 token,输出 ¥6/百万 token)估算,月度成本约 ¥30-80,取决于修复目标的数量和复杂度。巡检 prompt 重复度高,缓存命中率高时成本会更低。换成 GPT-4o 或 Claude 等模型,价格会高出一到两个数量级。

以上只是基于 demo 规模的粗略估算,实际费用取决于系统复杂度、错误数量和修复频率。

自进化的三个层次

前面讲的各个环节,按自动化程度可以分成三个层次:

层次人做什么系统做什么本文覆盖
L1 纯人工看日志、分类错误、写修复代码
L2 辅助确认分类结果 + 决定是否修复自动采集数据、找出可疑错误
L3 主导审查 PR + 高风险决策采集 → 识别 → 分类 → 生成修改 → 提 PR本文主要演示
1flowchart LR
2    subgraph L1[L1 纯人工]
3        A1[人看日志] --> A2[人分类] --> A3[人修复]
4    end
5    subgraph L2[L2 辅助]
6        B1[系统采集数据] --> B2[系统找出可疑错误] --> B3[人确认 + 人修复]
7    end
8    subgraph L3[L3 主导]
9        C1[系统采集] --> C2[系统识别] --> C3[系统分类] --> C4[系统生成修改] --> C5[系统提 PR] --> C6[人审查 PR]
10    end
11    L1 -.->|加 Tracing| L2
12    L2 -.->|加 LLM 巡检 + 自动修复| L3
13

表里的三个层次对应上面“迁移到你的项目”里的步骤:加完 Tracing 就到 L2,再接上 LLM 巡检和自动修复就到 L3。

重放引擎:自动验证修复是否生效

目前系统最大的短板是:自动修复生成了 PR,但 PR 到底有没有修好 bug,还是得靠开发测试和审核。重放引擎要解决的就是这个问题,让系统自己验证修复是否有效。

为什么确定性重放可行

回顾 Tracing 那节:steps 表记录了每一步的输入输出——llm_response 存了模型回复,tool_output 存了工具返回的 JSON。重放时不需要调用真实的 LLM、不执行真实的工具,把录好的数据按步骤顺序传给 Agent Loop 就行,整个执行过程就变成了纯函数。

这对 harness_bug 特别有效。这类 bug 出在 Harness 怎么处理返回值,跟返回值本身无关。比如“token 接近上限时没触发压缩,下一轮 API 调用就失败了”——不管模型说了什么,只要上下文管理的逻辑有问题,bug 就会出现。重放同样的 LLM 回复和工具返回值,大概率能复现。

实现方案:复用同一个 agentLoop

不用写一套新的执行引擎。重放走的还是同一个 agentLoop(),只是把 LLM 和工具替换成直接返回录好数据的 mock 版本。

先看正常执行时 agentLoop 怎么工作的:

1sequenceDiagram
2    participant AL as agentLoop
3    participant LLM as 真实 LLM API
4    participant T as 真实工具
5
6    AL->>LLM: 发送消息
7    LLM-->>AL: &#34;调用 dbQuery&#34;
8    AL->>T: 执行 dbQuery(SQL)
9    T-->>AL: 查询结果 JSON
10    Note over AL: Harness 处理返回值<br/>(上下文管理、错误恢复等)
11    AL->>LLM: 回传工具结果
12    LLM-->>AL: 生成最终回复
13

重放时,把 LLM 和工具都换掉,但 agentLoop 本身的代码不需要修改:

1sequenceDiagram
2    participant AL as agentLoop(同一份代码)
3    participant SP as ScriptedProvider
4    participant TR as ToolReplayer
5
6    Note over SP:  steps 表读取<br/>录好的 llm_response
7    AL->>SP: 发送消息
8    SP-->>AL: 返回录好的 &#34;调用 dbQuery&#34;
9    Note over TR:  steps 表读取<br/>录好的 tool_output
10    AL->>TR: 执行 dbQuery(SQL)
11    TR-->>AL: 返回录好的查询结果 JSON
12    Note over AL: Harness 处理返回值<br/>(被测试的就是这部分)
13    AL->>SP: 回传工具结果
14    SP-->>AL: 返回录好的最终回复
15

ScriptedProvider 实现了和真实 LLM 一样的接口(LanguageModelV1),但不连上游 API,直接从 steps 表按步骤顺序返回录好的 llm_response。ToolReplayer 替换工具注册表,每次 tool_call 直接返回录好的 tool_output,不发邮件、不执行代码、不请求外部 API。

agentLoop 的视角看,重放和真实执行没有区别——它不知道对面是真 LLM 还是录像回放。被测试的就是 agentLoop 拿到返回值之后的处理逻辑:上下文管理有没有正确触发压缩、工具结果有没有正确截断、错误恢复有没有漏掉边界情况。

与自动修复的集成

auto-pr.ts 生成代码修改后、push 之前,加一道验证(Safety Gate):

复现验证分两步:先用没打补丁的代码重放触发 bug 的 operation——必须失败(确认 trace 能复现 bug);再用打了补丁的代码重放同一条——必须成功(确认修复有效)。如果第一步就成功了,说明这个 bug 可能跟 Harness 之外的条件有关,重放验证不了,跳过。回归验证是拿打过补丁的代码去跑之前修过的 bug 对应的 trace,全部通过才允许提交。

回归语料库的自动积累

每当一个 harness_bug 的修复 PR 合并后(fix_statusmerged),触发这个 bug 的那条 trace 会自动存进一个回归用例集合。之后每次生成新的修复 PR,除了验证新 PR 本身能修好目标 bug,还要把回归用例集合里已有的 trace 全部重放一遍,确保新改动没有把以前修好的 bug 重新引入。修好的 bug 越多,回归用例集合越大,回归保护的覆盖面也越广。

边界

重放引擎验证的是 Harness 的处理逻辑,不是 LLM 的回答质量。因为重放时 LLM 的回复是录好的固定数据,所以模型回答得好不好不在验证范围内。重放引擎只关心一件事:Harness 拿到这些返回值之后,上下文管理、工具结果处理、错误恢复这些代码逻辑有没有正确执行。

由于复杂度的问题,我没有在 demo 里实现重放引擎,不过这个确实是可行的,因为我之前在别的产品已经实现过了。

做了这些之后能得到什么

读者可以在 demo 里直接验证这套流程的效果。先执行 pnpm simulate --mock 注入模拟数据(也可以不加 --mock 参数,用真实 API 调用),然后在管理面板跑一次巡检。因为这时候 Pattern 库还是空的,所有错误都处于"未匹配"状态。巡检执行后,系统会把相似的错误归到一起,为每组错误生成对应的 Pattern,再拿新 Pattern 去回扫之前的历史错误,把能匹配上的都标记上。这样一来,原先全是"未匹配"的错误列表里大部分错误都已经被分类,未匹配错误率大幅下降。

但巡检只是识别和分类错误,真正让错误减少的是后续的自动修复。巡检把 harness_bug 类型的 Pattern 标记出来之后,自动修复会为这些 bug 生成修复 PR。开发者审核并合并 PR 后,对应的 bug 在后续的请求中就不会再出现了。随着巡检不断发现新的 bug、自动修复不断生成 PR、开发者不断合并修复,系统产生的错误会越来越少,需要人工处理的问题也越来越少。

这套思路来自 LobeHub 的生产实践(《需要自进化的不是 Agent,而是 Harness》),他们公布的数据:

  • 经过 9 轮巡检,Pattern 从 31 条增长到 104 条后趋于饱和,每轮新增的 Pattern 数量从 31 持续下降到 0
  • 巡检过程中发现了 20 多个 Harness 自身的缺陷,包括 Schema 不兼容、负数 max_tokens、reasoning_content 丢失、Context Window 过载等
  • Agent 成功率从早期的约 75% 提升到 95% 以上

这套机制运行起来之后,每次请求产生的错误都会被 trace 记录下来,下一轮定时巡检自动把它归类到已有的 Pattern 或者生成新的 Pattern。同一类错误只需要开发者审核一次(确认 Pattern 分类是否准确),之后再出现同类错误时系统直接匹配,不需要开发者再介入。Pattern 库随着巡检轮次不断积累,未匹配的错误会越来越少。

我的其他 AI Agent 文章

参考资料


让 AI Agent 系统自己发现 bug、自己提修复 PR:自我进化的 Harness》 是转载文章,点击查看原文


相关推荐


【节点】[Rectangle节点]原理解析与实际应用
SmalBox2026/6/20

【Unity Shader Graph 使用与特效实现】专栏-直达 Rectangle 节点是 Unity URP Shader Graph 中一个功能强大的形状生成工具,专门用于在着色器中创建矩形图案。该节点基于输入的 UV 坐标生成矩形形状,通过精确的参数控制可以实现从简单的方形到复杂矩形图案的创建。在游戏开发中,矩形形状有着广泛的应用场景,包括创建 UI 元素、制作平铺纹理、构建几何图案、实现遮罩效果等。 该节点的核心工作原理是通过数学计算在 UV 空间中定义矩形区域。UV 空间是一个标


LangChain上手 MCP:从用别人工具到自己写工具
颜酱2026/6/12

LangChain上手 MCP:从用别人工具到自己写工具 读完这篇文章,你将能:用别人的 MCP Server(比如百度地图)、自己写一个 MCP Server(比如计算器)、用 Agent 自动调度多个工具。 先看效果 用别人的:百度地图 MCP 十几行代码,让 AI 变成地图助手: import asyncio from langchain_mcp_adapters.client import MultiServerMCPClient from langchain.agents imp


女程序媛多肉的 AI 小绿书,短短 3 天 220 粉
threerocks2026/6/5

微信公众号近 1~2 年,主推贴图类型作品,并且对这类内容有比较明显的流量倾斜。再叠加这两年 AI 生图的成熟,Nano Banana Pro 和 GPT-Image-2 先后出世,既然图片生产不再是卡点,那很多人会很自然地想到一个项目:用 OpenClaw、Codex、Claude Code 这类 Agent 工具,把公众号贴图自动化做起来。 理论上,这听起来像一个很好且成本不高的生意。AI 负责生图,Agent 负责写文案,公众号负责推荐流。人只要选图、点发布,甚至再往后一点,连选图都可以交


AI 大模型核心五:从 Transformer、RAG 到 Agent 架构
zhangxingchao2026/5/29

一、如果去掉多头只用单头,Transformer 会出现什么问题? 一句话概括: 单头注意力会把所有关系都压到一个注意力分布里,导致模型很难同时关注语法、语义、位置、指代、长程依赖等多种信息,表达能力会明显下降。 这里要注意,单头不是完全不能用,而是会形成明显的信息瓶颈。 1. 多头注意力到底解决什么问题? Multi-Head Attention 的关键价值,不只是“多算几次 attention”,而是让不同的 head 在不同子空间里学习不同类型的关系。 一段文本里,模型可能同时需要关注


深度学习(13)PyTorch神经网络基础
β添砖java2026/5/7

1. 层和块 ① nn.Sequential 定义了一种特殊的Module。 # 回顾一下多层感知机 import torch #基础计算(类似 numpy) from torch import nn #神经网络工具(层、模型) from torch.nn import functional as F #一些函数(比如激活函数) #搭建神经网络结构 net = nn.Sequential(nn.Linear(20,256),nn.ReLU(),nn.Linear(256,10))


Claude Code 从零上手:国内用户保姆级安装教程
易安说AI2026/4/27

Claude Code 是目前公认最强的 AI  编程 Agent 框架。很多人以为它必须配合 Claude 官方模型才能用,但实际上,Claude Code 本质是一个 Agent 框架,搭配任何模型都能运行。本文手把手教你从零安装 Claude Code,并用国产模型 GLM-5.1 接入,全程不需要海外手机号、Visa 卡,甚至不需要代理。  操作系统:macOS 或 Windows 均可 网络:有代理最好,没有也能用(本文两种方案都覆盖) 模型选择:推荐 GLM-5.1(国内效果最接近


js的深拷贝和浅拷贝?啥情况讲解下??底层堆栈空间??object.prototype.toString.call(),还有bind,的具体使用?
神の愛2026/4/19

js的深拷贝和浅拷贝?啥情况讲解下??底层堆栈空间??object.prototype.toString.call(),还有bind,的具体使用?还有instanceof对象的实例,还有哪个方法可以直接获取到我定义的数据是什么类型的。哟哪几种数据类型?包括哪些? 1. 堆(Heap)与 栈(Stack):内存的真相 在 JS 里,内存被分为两块: 栈(Stack):空间小、速度快。存放基本数据类型(String, Number, Boolean, Null, Undefined, Sy


如何实现分布式锁
哈里谢顿2026/4/10

分布式锁是分布式系统中协调多个节点对共享资源访问的关键机制。我来从原理到实现,系统性地讲解。 核心要求 一个可靠的分布式锁必须满足: 特性说明互斥性同一时间只有一个客户端能持有锁防死锁锁必须有过期机制,避免客户端崩溃后锁永远不被释放可重入性(可选)同一客户端可以多次获取同一把锁容错性大部分节点存活时,锁服务仍能正常工作 方案一:基于 Redis(最常用) 1. 基础版(SETNX + EXPIRE) SETNX lock:resource "


Ospf网络类型:P2P和Broadcast
24zhgjx-fuhao2026/4/2

一、P2P (一)、P2P的作用及特点 P2P(点到点)网络类型 1.作用: 适用于连接两台路由器的链路‌,例如通过PPP(点对点协议)或HDLC(高级数据链路控制)封装的串行链路。不需要进行DR(指定路由器)和BDR(备份指定路由器)的选举。路由器之间可以直接建立邻接关系(Full状态),无需通过DR/BDR进行LSA(链路状态通告)的泛洪。 2.特点: 无需选举DR/BDR‌:因为只有两个设备通信。自动发现邻居‌:通过发送Hello报文自动发现邻居。组播发送协议报文‌:使用组播地


Linux中基础IO知识全解
顶点多余2026/3/25

1.什么是文件 文件是指内容+属性,所以文件永远不是只有内容; 1.1狭义上: ①文件的存储特性 永久性存储:磁盘是永久性存储介质,文件在磁盘上的存储是永久性的(断电不丢失) ②磁盘的设备属性 外部设备:磁盘属于外设(I/O设备) 双重角色:既是输出设备(写入数据),也是输入设备(读取数据) ③文件操作的本质 I/O操作:对磁盘上文件的所有操作,本质上都是对外设的输入和输出 统称IO:所有文件读写操作,都可以简称为IO 1.2广义上: Linux 下⼀切皆⽂件(键盘、显⽰器、⽹卡、

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2026 聚合阅读