Jean's Blog

一个专注软件测试开发技术的个人博客

0%

Harness Engineering:八类应对机制

上一节介绍了智能体的八种常见失效方式,以及 Harness Engineering 中对应的治理思路。本章进一步说明这些机制的目标、边界和最小实现。

mindmap
  root["Harness Engineering 驾驭机制"]
    ["1.Agent Loop 四相循环"]
      ["解决故障:循环失控"]
      ["核心逻辑:四段闭环、最大迭代硬限制、双重终止条件"]
      ["类比:驾校开车标准化流程"]
      ["代码要点:循环内置完成状态判断"]
    ["2.Tool Use 工具编排"]
      ["解决故障:工具异常被静默吞噬"]
      ["核心逻辑:统一 Schema、结构化错误、稳定的工具契约"]
      ["类比:USB-C标准接口统一故障码"]
      ["代码要点:统一封装safe_tool_call"]
    ["3.Progress Tracking 进度追踪"]
      ["解决故障:进程中断状态丢失"]
      ["核心逻辑:JSON持久化、断点续跑"]
      ["最佳实践:进度文件+Git双Checkpoint"]
      ["类比:游戏自动存档"]
      ["代码要点:save_progress / load_progress"]
    ["4.Context Management 上下文管理"]
      ["解决故障:上下文溢出、Prompt缓存失效"]
      ["核心逻辑:Token监控、固定System Prompt"]
      ["策略:历史摘要压缩 / 重置上下文"]
      ["踩坑警告:系统提示词禁止写入动态信息"]
      ["类比:工位文件归档清理"]
    ["5.Feature List 任务拆解"]
      ["解决故障:上下文溢出(源头缓解)"]
      ["核心逻辑:任务清单拆分,单次仅执行一项任务"]
      ["清单持久化到磁盘,按需读取以节省上下文"]
      ["类比:Trello待办看板"]
      ["代码要点:get_next_task获取未完成任务"]
    ["6.Verification Loop 验证闭环"]
      ["解决故障:工具报错后AI虚假自洽成功"]
      ["核心逻辑:真机自动化测试,以返回码为准"]
      ["约束:禁止修改测试用例掩盖bug"]
      ["类比:QA验收测试"]
      ["代码要点:subprocess执行自动化用例"]
    ["7.Subagents 子代理分治"]
      ["解决故障:上下文溢出(分流方案)"]
      ["核心逻辑:子代理独立上下文,隔离中间日志"]
      ["类比:CEO外包专业顾问,仅接收最终报告"]
      ["代码要点:sub_messages全新消息列表"]
    ["8.Generator-Evaluator三角色架构"]
      ["解决故障:无独立自动化评审、自评偏袒"]
      ["核心逻辑:规划/生成/评审角色分离、对抗校验"]
      ["类比:论文审稿机制"]
      ["代码要点:独立Evaluator调用LLM校验产出"]

机制 X 故障模式 对照矩阵

机制 \ 故障 循环失控 Context 溢出 Cache Miss Tool 错吞 状态丢失 缺权限闸 缺自动化评审 成本失控
Agent Loop四相循环 主解 - - - - -
Tool Use工具编排 - - - 主解 - - - -
Progress Tracking进度追踪 - - - - 主解 - - -
Context Management上下文管理 - 主解 主解 - - - -
Feature List任务拆解 - 源头 - - - - -
Verification Loop验证闭环 - - - 下游 - - - -
Subagents子代理分治 - 另一解 - - - - -
Generator-Evaluator生成 - 评估对抗 - - - - - - 主解 -

以下是对应的故障机制的详解。

Agent Loop 四相循环(循环失控)

怎么让一个循环不失控

一句话定义:把一次 agent 迭代拆成 Gather Context(收集上下文)→ Take Action(执行动作)→ Verify(验证结果)→ Iterate(迭代改进)四个阶段,并硬性加上 max_iterations 和显式终止条件。

flowchart TD
    %% 中心圆形节点:Agent Loop
    Center((Agent
Loop)) %% 四个流程节点 G["Gather Context
(收集上下文)"] T["Take Action
(执行动作)"] V["Verify
(验证结果)"] I["Iterate
(迭代改进)"] %% 顺时针闭环 G --> T T --> V V --> I I --> G %% 配色贴近原图 style G stroke:#64748b style T fill:#fffbeb,stroke:#d97706 style V fill:#f0fdf4,stroke:#059669 style I fill:#f5f3ff,stroke:#6d28d9 style Center stroke:#aaa,fill:#f8f8f8 %% 顶部文本标注(多数渲染器支持note) note["🔴 HARNESS BOUNDARY: MAX_ITERATIONS (拦截保护层)"]

参考资料Building agents with the Claude Agent SDK(Anthropic,2025-09-29,作者 Thariq Shihipar)。

解哪条故障:故障 ① 循环失控——naive agent 的 while True 在这里被替换成 for i in range(MAX_ITER),每轮还有显式的 verify 阶段做”能不能停”的判断。

生活化类比驾校教练教你开车的四步口诀——“看路况 → 踩油门 → 看前方 → 调整方向”。每一步都有明确动作,开完一圈看有没有到终点;到了就停、没到就再来一圈——但你不会无限圈兜下去,因为教练给你定了”跑 10 圈就必须停”的硬规则。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 机制:Agent Loop 四相循环
# 解 ① 号故障:带 max_iterations 硬刹车 + 显式终止条件

# —— 下列四个函数 + state.is_done() 是"四相"的教学标签 ——
# 在真实 agent 里对应到 cell [55] naive_agent 的具体片段:
# gather_context(state) ≈ 构造/读取 messages(见 cell [55] messages 初始化)
# take_action(context) ≈ client.chat.completions.create(...)(见 cell [55] LLM 调用)
# verify(action) ≈ 机制 6.6 的 verify_with_pytest(真机验证)
# iterate(state, r) ≈ messages.append(...) + state 更新
# state.is_done() ≈ msg.tool_calls 为空时的 break 条件
MAX_ITER = 20 # 硬性上限,到点必停

def agent_loop(initial_state):
"""
四相循环主函数

Args:
initial_state: 初始状态对象(含任务、历史、已完成标记)
Returns:
final_state: 循环结束时的最终状态
"""
state = initial_state
# for 循环代替 while True,保证最大迭代次数
for i in range(MAX_ITER):
context = gather_context(state) # 相 1:收集上下文
action = take_action(context) # 相 2:执行动作
result = verify(action) # 相 3:验证结果
state = iterate(state, result) # 相 4:迭代状态
# 显式终止条件:状态自报完成才退出
if state.is_done():
break
return state

这段代码里最关键的一行是 for i in range(MAX_ITER) 取代了 naive agent 的 while True。我们可能会觉得”这不就是加了个计数器吗”——对,就是加了个计数器。但这个计数器背后的工程思维是:agent 不该完全信任模型的自我终止判断,系统必须有一个强制出口。Anthropic 官方博文里用的词是 “structured loop with guaranteed exit”——有保证的出口,这就是我们的第一道刹车。

Tool Use工具编排(Tool错误处理为空字符串)

工具失败了 agent 要看得见

一句话定义:所有工具调用包裹在结构化 schema 里(名字、参数、返回格式统一),失败时返回 {"status": "error", "error": "..."} 这样的结构化回传,而不是空字符串或异常被吞

参考资料Building agents with the Claude Agent SDK(Anthropic,2025-09-29)。MCP(Model Context Protocol)则是连接 AI 应用与外部数据源、工具和工作流的开放协议。它与单次 Function Calling 的工具 schema 有交集,但两者不是简单的“标准版 / 非标准版”关系。

解哪条故障:故障 ④ tool 错误处理为空字符串——naive agent 里 except: result = "" 是典型症状,修复方式是把 error 变成结构化 return,而不是字符串或异常。

生活化类比USB-C 接口。任何设备插上 USB-C 线,host 都能通过标准协议读到对方的身份、能力、故障码;不兼容的设备也能返回”设备不识别”,而不是一条死线静默。工具 schema 就是 agent 世界的 USB-C。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 机制:Tool Use 工具编排
# 解 ④ 号故障:工具失败必须结构化回传,agent 才能感知

# TOOL_REGISTRY 示例:即 cell [55] 三个 tool 函数的字典封装,集中查表
TOOL_REGISTRY = {"read_file": read_file, "write_file": write_file, "run_pytest": run_pytest}


def safe_tool_call(tool_name: str, **kwargs) -> dict:
"""
结构化工具调用包装器

Args:
tool_name: 工具名(从 TOOL_REGISTRY 查表)
**kwargs: 工具参数(JSON schema 约束)
Returns:
dict: {"status": "ok"|"error", "result": 或 "error": 字符串}
"""
try:
# 成功路径:正常返回结果
result = TOOL_REGISTRY[tool_name](**kwargs)
return {"status": "ok", "result": result}
except Exception as e:
# 关键改进:error 不是空字符串,agent 能感知失败
return {"status": "error", "error": str(e)}

对比 naive agent 里的 except: result = "" 和这段代码的 return {"status": "error", "error": str(e)}——两者差别只有一行,但工程含义完全不同。前者是”失败了装作没事”,后者是”失败了把失败本身当作第一公民返回”。agent 下一轮看到 status: error 才会触发修复路径,看到空字符串就以为成功。结构化错误是 agent 能跑长任务的第一块基石

Progress Tracking 进度追踪(状态丢失)

kill 进程或者关电脑之后,agent 能不能从断点接着跑

一句话定义:把每一步的进度写到一个持久文件(Anthropic 官方用的是 claude-progress.txt)+ 在完成里程碑时打 git commit,这样即使 session 断了,重启时能读取进度文件从断点续传。

参考资料Effective Harnesses for Long-Running Agents(Anthropic,2025-11-26,作者 Justin Young)。原文原话:”a claude-progress.txt file that keeps a log of what agents have done”。

生活化类比游戏存档。开放世界游戏不可能指望玩家一次通关,必须每过一个小任务自动存档;agent 跑长任务也是同样——每完成一个子步骤就”存档”,断了能”读档”接着跑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 机制:Progress Tracking
# 解 ⑤ 号故障:跨 session 续传的最小基础设施
import json
import pathlib

PROGRESS_FILE = pathlib.Path("claude-progress.json")


def save_progress(state: dict) -> None:
"""
每步完成后写进度文件,失败后可续传

Args:
state: 当前 agent 状态(含已完成任务、上下文快照)
"""
# 每步写磁盘,写 JSON 格式方便人类阅读和工具消费
PROGRESS_FILE.write_text(json.dumps(state, indent=2, ensure_ascii=False))


def load_progress() -> dict | None:
"""
启动时先读进度,有则续传

Returns:
dict 或 None:有进度则返回状态,无则返回 None(全新开始)
"""
# 存在即续传,不存在即从零开始
if PROGRESS_FILE.exists():
return json.loads(PROGRESS_FILE.read_text())
return None

这段代码看上去简单到不值得写成独立机制——不就是 JSON 读写吗?但我们需要想清楚一件事:没有它,agent 跑 30 分钟失败一次就得从零开始。Anthropic 官方博文给出的最佳实践更强:progress 文件 + git commit 双重 checkpoint——前者记录 agent 的思考过程,后者记录代码的物理改动,两个维度的断点都可以续上。

Context Management 上下文管理(context 溢出 / cache miss)

context window 要溢出怎么办

一句话定义:持续监控当前 messages 的 token 数,超过阈值时触发压缩(compaction)或重置(reset),同时保持 system prompt 前缀稳定不变以命中 prompt cache。

参考资料Automatic Context Compaction Cookbook(Claude Agent SDK)。压缩、裁剪和新会话恢复是可组合的策略,不应描述成厂商已经从 compaction 单向迁移到 reset。

解哪条故障:一次救两条——故障 ② context 溢出(压缩能降维度)和故障 ③ cache miss(保持前缀稳定才能命中 cache)。

生活化类比工位清理。你工位上的文件堆到爬不动的时候,有两种策略:压缩(把过期文件归档成一页摘要)重置(今天新文件从空桌开始)。agent 的 context 也一样,到某个阈值就得做出这个选择。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 机制:Context Management
# 解 ②③ 号故障:超阈值时压缩非前缀部分,保持 system prompt 前缀不变
CONTEXT_THRESHOLD = 80_000 # token 阈值(示意值,按模型实际 window 调整)


def call_llm_for_summary(msgs: list) -> str:
"""
把一串 messages 压成一段摘要

真实实现会调 LLM 生成带语义的总结;此处用最朴素的"拼接+截断"演示"压缩动作的形态",
让学员看清 messages 从 N 条 → 1 条 summary 的数据流,而不是 LLM 本身怎么总结。

Args:
msgs: 待压缩的消息列表(不含 system prompt)
Returns:
str: 摘要字符串
"""
# 仅演示形态:把所有 content 拼接后截断;真实场景请换成 LLM 摘要
joined = " | ".join(str(m.get("content", ""))[:80] for m in msgs)

return f"[前 {len(msgs)} 条消息压缩摘要] {joined[:500]}"


def manage_context(messages: list, token_count: int) -> list:
"""
上下文管理器:超阈值触发压缩

Args:
messages: 当前消息列表(OpenAI 格式)
token_count: 当前总 token 数(由上层调用方估算)
Returns:
list: 处理后的 messages(未超阈值则原样返回)
"""
# 未超阈值直接返回,保持 prompt cache 命中
if token_count <= CONTEXT_THRESHOLD:
return messages
# 超阈值才触发压缩;system prompt(messages[0])永远不动以保 cache
summary = call_llm_for_summary(messages[1:])
# 返回前缀稳定 + 压缩摘要的新 messages
return [messages[0], {"role": "assistant", "content": summary}]

这段代码里最容易被忽略的是 messages[0] 这一行——system prompt 永远不动。为什么?因为 prompt cache 是按前缀匹配的,前缀一变整个 cache 作废,延迟翻倍、费用翻倍(解 ③ 号故障)。很多 Agent 开发者第一反应是”在 system prompt 里加时间戳或当前任务”,这是 cache miss 最常见的起因。正确做法是:system prompt 是恒定的”角色定义”,当前任务进 user 消息,永远不要动前缀。

【踩坑预警】 在 system prompt 里加时间戳、用户名、当前任务
后果:很多开发者把当前任务状态、时间戳或 task_id 写进 system prompt,以为”让模型知道当前在做什么”更好——实际上这会让每轮的 system prompt 前缀都不同,prompt cache 全部失效,延迟和费用双双翻倍。在一个长任务里,这项失误的成本可能是数十美元。
正确做法:system prompt 只放固定的”角色定义”(永不变),当前任务、时间、task_id 全部放进 user 消息。
排查方法:看 API 返回里的 cached_tokens 字段——如果你在多轮循环中这个字段始终是 0 或远小于 system prompt 的 token 数,说明前缀被改动了、cache 没命中。

【常见误区】 把 context window 当成”memory”
后果:很多初学者以为 “context 越大 agent 记得越多”,于是把所有历史对话都塞进 context 等同于”长期记忆”——实际上 context window 只是一次请求的短期工作区,超阈值必须清理;跨 Session 信息需要由进度文件、Checkpoint、数据库或其他持久化机制承担。
正确做法:把 context window 当”工位”(本次会话要用的文件)、把 progress 文件当”档案柜”(跨 session 持久的记忆);需要 Agent 跨天保留的信息应写入可靠的外部存储,而不能只依赖当前 Context。

Feature List 任务拆解(context 溢出的源头)

解决的是 context 溢出的源头——一次让 agent 做太多事

一句话定义:把大任务拆成 JSON 格式的清单([{id, task, status}]),强制 agent 单次迭代只做一件事,每件事做完标记状态再取下一件。

参考资料Effective Harnesses for Long-Running Agents(Anthropic,2025-11-26)。原文给出了完整的 JSON feature 结构示例,passes 字段标注 true/false。

解哪条故障:故障 ② context 溢出的源头——naive agent 一次把 “读文件 + 修 bug + 跑 pytest” 全塞进去,context 自然爆。Feature List 把它切成 3 个独立子任务,每个子任务 context 小得多。

生活化类比待办清单。你早上不会一口气处理 10 件事,而是列一个清单、做完一件划掉一件。agent 也一样——清单是它的外置工作记忆,不依赖 context window。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 机制:Feature List 任务拆解
# 解 ② 号故障的源头:强制单次只做一件事
import json

# 典型 feature list 结构:id / task / status 三字段
feature_list = [
{"id": 1, "task": "读取 test_calculator.py", "status": "pending"},
{"id": 2, "task": "修复 add 函数的整数溢出 bug", "status": "pending"},
{"id": 3, "task": "运行 pytest 验证全部通过", "status": "pending"},
]


def get_next_task(features: list) -> dict | None:
"""
取下一个 pending 任务,防止 agent 贪心多做

Args:
features: feature list(字典列表)
Returns:
dict 或 None:最早的 pending 任务,全部完成则 None
"""
# 按 id 顺序取第一个 pending,强制单步执行
# 这里用 next + 生成器是为了短路——找到第一个立即返回,不遍历全表
# 如果 agent 想"一次做多件",这个函数只返回一件,它就不得不分多轮
return next((f for f in features if f["status"] == "pending"), None)

这个机制可以把它理解成 agent 世界的 Trello 看板。每个 task 是一张卡片、有明确的 pending / in_progress / passes 状态。Anthropic 的示例将清单保存在 JSON 文件中,以磁盘内容作为事实来源;Agent 执行时仍需按需把相关任务读取到 Context。

Verification Loop 验证闭环(Tool 错误的下游)

跟 Tool Use是一组搭档——Tool Use管”工具层面的结构化错误”,Verification Loop任务层面的真机验证”

一句话定义:每个 feature 完成后,用 Playwright / Puppeteer / pytest 等真机工具去实际验证功能,而不是只看 LLM 自己说”我觉得做好了”。验证失败就回退重做,验证通过才标记 passes=true。

参考资料Effective Harnesses for Long-Running Agents(Anthropic,2025-11-26)。原文明确提到 Puppeteer MCP server 用于前端功能验证,以及一句非常狠的规则:”It is unacceptable to remove or edit tests because this could lead to missing or buggy functionality”(不可接受删掉或修改测试,因为这会导致功能缺失或带 bug)。

解哪条故障:故障 ④ tool 错误吞的下游——光在工具调用层面返回 error 不够,任务层面必须有”真机验证”才能确认功能真的跑通。

生活化类比QA 验收测试。开发说”我修好了”不算数,得 QA 真的点按钮、真的跑场景、真的截图对照才算数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 机制:Verification Loop
# 解 ④ 号故障的下游:任务层面真机验证,不只看 LLM 自评
import subprocess


def verify_with_pytest() -> dict:
"""
pytest 真机验证

Returns:
dict: {"passed": bool, "output": str}
"""
# 真跑 pytest,不 mock 不 skip(mock 会让 agent 看到"假通过")
# --tb=short 让错误回溯简短,避免 stdout 溢出塞满 context
result = subprocess.run(
["pytest", "--tb=short"], capture_output=True, text=True
)
# 关键:returncode 不丢弃,作为 passed 判定依据
# stdout + stderr 合并返回,让 agent 能看到完整错误信息
return {
"passed": result.returncode == 0,
"output": result.stdout + result.stderr,
}

对比 naive agent 的 run_pytest 函数(它丢掉了 returncode),这里显式保留 result.returncode == 0 作为 passed 判定——“通过”必须来自外部真实反馈,而不是 agent 自己说通过了。Anthropic 官方还额外约束:”不能改测试让它通过”——如果 agent 发现测试在报错,正确路径是”去修实现”,错误路径是”把测试改成能过”。这是保证验证闭环不被 agent 作弊的最后一道防线。

Subagents 子代理分治(context 溢出的另一种解法)

context 溢出第二种解法——和 Context Management压缩、Feature List拆分互补

一句话定义:把特定子任务派发给一个独立 context 的子 agent,子 agent 在自己的 messages 列表里工作、完成后只把结果返回主 agent,主 Agent 通常只接收子任务结果,从而减少中间工具日志和试错过程对主 Context 的占用。

参考资料Building agents with the Claude Agent SDK(Anthropic,2025-09-29)。Claude Code 等编码 Agent 可以通过子 Agent 将任务放入独立上下文(参见 Effective Harnesses 中关于 Subagents 的讨论)。

解哪条故障:故障 ② context 溢出的另一解法——子 Agent 可以在独立 Context 中处理多轮试错和大量工具调用,主 Agent 主要接收结构化结果,但仍需承担任务编排和结果整合成本。

生活化类比外包给顾问。CEO 把一块专业问题外包给咨询顾问,顾问自己开了一个独立项目(独立资料库、独立讨论),几周后只交一份结论给 CEO。CEO 的主 context 没有被顾问讨论的中间文档污染。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 机制:Subagents 子代理分治
# 解 ② 号故障:独立 context 隔离,主 agent 不被子任务中间过程污染

# —— run_loop 即机制 6.1 的 agent_loop(完整四相循环) ——
# 重点不在 run_loop 内部怎么跑,而在 sub_messages 是全新列表,与主 agent 完全隔离:
# 主 agent 看不到子任务的中间 tool_calls 轨迹,只拿得到 subagent 的最终 return 值。
def subagent(task: str, tools: list) -> str:
"""
子代理:独立 messages 列表完成子任务

Args:
task: 子任务描述(自然语言)
tools: 子代理可用工具清单
Returns:
str: 子任务的最终结论(不含中间过程)
"""
# 子代理有独立的 messages,不共享主 agent 历史
sub_messages = [
{"role": "system", "content": "你是专注于单一子任务的助手"},
{"role": "user", "content": task},
]
# 内部跑一个完整的 agent loop,但外部看不到中间步骤
return run_loop(sub_messages, tools)

注意 sub_messages = [...] 这一行——它不是从主 agent 的 messages 复制过来的,而是全新的独立列表。这是 Subagents 机制的核心:context 是隔离的。主 agent 拿到的只有子 agent 的最终 return 值,中间子 agent 跑了 20 轮工具调用主 agent 一无所知——正因为”一无所知”,主 context 才能保持干净。

Generator-Evaluator(三角色分工与独立评审)

它不再是在单 agent 上打补丁,而是直接引入多 agent 架构

Anthropic 的文章说该架构“受到 GAN 启发”,但这里并不存在 GAN 的训练过程、损失函数或参数更新。更准确的理解是:把规划、生成和评审职责分开,用独立评审减少生成者自评带来的偏差。

一句话定义:把任务处理流拆成三个角色:Planner(规划者)拆任务,Generator(生成者)完成实现,Evaluator(评估者)依据独立标准检查产物。评估输入应尽量聚焦可观察结果和验收标准,而不是接受生成者的自我声明。

参考资料Harness Design for Long-Running Application Development(Anthropic,2026-03-24,作者 Prithvi Rajasekaran)。原文原话:”Taking inspiration from Generative Adversarial Networks (GANs)”,并明确设计了 three-agent architecture—planner, generator, and evaluator

解哪条故障:故障 ⑦ 缺自动化评审——naive agent 里没有任何独立评审角色,agent 自己说做完就做完;Generator-Evaluator 架构把”评审”作为一个独立 agent 机械执行的步骤,打破”生成者自评偏见”。

生活化类比论文评审制度。作者(Generator)写论文,审稿人(Evaluator)不看作者的写作过程只看论文本身,按评分标准打分;编辑(Planner)在作者和审稿人之间做协调,决定修改方向和是否录用。三个角色分离才能保证论文质量,让作者自己评自己的论文质量无法保证。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 教学示例:真实项目应把 call_llm 替换为模型客户端,并启用结构化输出。
import json
from typing import Any


def call_llm(prompt: str) -> str:
raise NotImplementedError("请接入真实模型客户端")


def planner(task: str) -> list[str]:
raw = call_llm(
"将任务拆成 JSON 字符串数组,每项都必须可独立验收:\n" + task
)
tasks = json.loads(raw)
if not isinstance(tasks, list) or not all(isinstance(item, str) for item in tasks):
raise ValueError("Planner 必须返回字符串数组")
return tasks


def generator(subtask: str) -> str:
return call_llm(f"完成以下子任务,并返回最终产物:\n{subtask}")


def evaluator(artifact: str, criteria: str) -> dict[str, Any]:
raw = call_llm(
"仅依据验收标准评估产物,返回 "
'{"approved": true|false, "feedback": "..."}:\n'
f"验收标准:{criteria}\n\n产物:{artifact}"
)
result = json.loads(raw)
if not isinstance(result.get("approved"), bool):
raise ValueError("Evaluator 缺少布尔字段 approved")
return result

示例要求 Planner 和 Evaluator 返回结构化 JSON,并对结果做最小校验。真正的可靠性仍应来自外部证据,例如 pytest、类型检查、静态分析或浏览器测试;“换一个 Agent 再评一次”只能降低部分自评偏差,不能替代确定性验证。

走到这里,我们已经把八大机制的核心逻辑过了一遍。手里现在有:Agent Loop 的四相刹车(救①)/ Tool Use 的结构化错误(救④)/ Progress Tracking 的游戏存档(救⑤)/ Context Management 的工位清理(救②③)/ Feature List 的待办清单(救②源头)/ Verification Loop 的 QA 闸(救④下游)/ Subagents 的外包顾问(救②另一解法)/ Generator-Evaluator 的论文评审(救⑦)——这八个工具,就是你接下来看任何 Agent 产品时的解析框架。