CHAPTER 01 / Agent 工程
从一次对话到 Agent 循环
拆开模型、工具与运行框架,亲手跑通一个有证据的文件问答助手。
- 区分模型的决策能力与 harness 的执行职责
- 实现行动、观察、终止组成的最小循环
- 用运行轨迹定位事实、执行和停止错误
从一个可以验收的问题开始
贯穿这些章节的小项目叫 Repo 助手。用户问“这个项目的请求超时是多少秒?”资料里有 README、配置文件和测试说明。完成条件很具体:回答数值,说明生效条件,给出文件与行号。若资料没有答案,就说明缺少什么证据。先写清这些条件,后面才知道增加工具或换模型有没有帮助。
一次普通模型调用只能根据输入生成输出。若没有把配置放进输入,模型无法知道当前仓库的真实值。Repo 助手可以先搜索 timeout,再读取相关行,再决定是否要查配置覆盖规则。需要几次行动由得到的证据决定,这正是循环的价值。固定的“读文件后总结”也可能足够,应先把它作为基线。
模型决定下一步,harness 负责让事情发生
把模型想成一个返回“下一步提案”的组件。提案可以是调用工具,也可以是最终答案。harness 是外围程序,负责保存消息、解析提案、校验参数、调用真实工具、追加观察,并执行轮数与时间限制。模型输出 read_file 并不代表文件已经读过;只有工具返回结果,证据才进入本次运行。
调试时要拆开两类问题。模型选错文件是决策问题;工具读取了正确文件却返回空内容是执行问题。换一个更强模型通常修不好路径拼接错误。反过来,工具完全正确也无法保证答案准确。记录模型版本、提示词版本和 harness 版本,才能比较两次实验。
真正接入模型时,只替换示例的 choose 函数,并把它的返回值转换为统一行动对象。不要顺便修改工具和退出条件,否则结果变化难以归因。模型输出可能无法解析,也可能一次提出多个工具调用,适配层应把这两种情况显式传给循环。先用固定响应回放这些分支,再测试真实调用,可以把网络不稳定与程序逻辑错误分开定位。
对文件问答而言,“模型输出最终答案”仅表示它请求停止。harness 仍可检查来源字段是否存在、路径是否在已读文件中;未通过就返回缺失项或终止为验证失败。这个检查不能证明语义一定正确,但能排除没有证据却直接结束的一类错误。
把运行状态设计成显式对象
一个最小状态包含任务、已收到的观察、剩余预算和终止原因。每条工具调用最好有 call_id,结果带来源与成功状态。之后即使工具并行完成,也能把结果对应回正确请求。不要把“未找到”与“工具失败”都表示成空字符串,前者是有效结果,后者表示还没有拿到证据。
ReAct 论文研究了推理与行动交替的方式。本教程借用“行动后根据环境反馈更新”的结构,不要求收集模型的内部思维过程。实际日志保存可观察的决策、参数、工具结果和最终产物就能支持很多调试工作。阅读 ReAct 原论文。
动手:一个完全离线的最小循环
下面是 Python 3 标准库示例,无需 API、网络或额外依赖。用确定性规则代替模型,目的是隔离并验证循环本身;它不是具有语言理解能力的完整 Agent。保存为 loop_demo.py 后运行 python3 loop_demo.py。
import json
FILES = {"config.txt": "timeout_seconds=30"}
state = {"task": "请求超时是多少秒?", "observations": []}
def choose(state):
if not state["observations"]:
return {"kind": "tool", "name": "read_file",
"arguments": {"path": "config.txt"}}
observed = state["observations"][-1]
if not observed["ok"]:
return {"kind": "final", "text": "没有读到配置,无法确认。"}
value = observed["text"].split("=", 1)[1]
return {"kind": "final", "text": f"超时为 {value} 秒。",
"source": observed["source"]}
for turn in range(3):
action = choose(state)
print(json.dumps({"turn": turn, "action": action}, ensure_ascii=False))
if action["kind"] == "final":
print("stop: completed")
break
path = action["arguments"]["path"]
result = {"ok": path in FILES, "text": FILES.get(path, ""),
"source": f"{path}:1"}
state["observations"].append(result)
print(json.dumps({"observation": result}, ensure_ascii=False))
else:
print("stop: step_budget_exhausted")正常运行会先打印 read_file,随后打印带 config.txt:1 来源的 30 秒答案。把 FILES 清空后,应得到无法确认的答案。这个变化说明回答取决于观察。把 choose 改成始终返回工具请求,循环会在预算耗尽时停止。
终止和恢复也属于产品行为
至少区分完成、预算耗尽、不可恢复错误和需要用户补充信息。超时退出不应被统计为成功。对于偶发读取失败,可以在总预算内重试;对于不存在的文件,重复读取通常不会改善结果,应搜索替代位置。连续两次同参数、同结果的调用可以触发无进展检测。
为开放任务设置最大轮数、总耗时和费用上限。上限过小会错过复杂任务,上限过大则让错误循环消耗资源。先用小任务建立正常调用次数分布,再决定预算。处理有副作用的工具时,失败后能否重试取决于是否确认动作已执行,不能一律自动重放。
如何读一条失败轨迹
假设答案写成 60 秒,按证据流倒着检查:最终答案是否误读了 30;观察是否读到了旧文件;搜索是否遗漏了生产配置;用户是否实际询问了另一个环境。沿着轨迹找到首次偏离任务的位置,修改相应组件。不要仅凭最终一句“出错了”就重写全部提示词。
一个实用的迭代节奏是先保留失败输入,再提出一个可检验假设,最后只改一个因素并回放。短提示、复杂规划和更多工具都不是天然优势。让同一套验收标准决定是否保留修改,下一章将把最容易出错的工具接口变得明确。
LEARN BY DOING
为 Repo 助手补齐三种退出路径
- 运行示例并保存成功轨迹;把配置值改成 45,观察答案是否同步变化。
- 模拟文件不存在与始终重复调用,分别产生证据不足和预算耗尽结果。
- 给每次工具调用添加唯一 call_id,日志写成每行一个 JSON 对象。
验收标准
- 正常配置回答 45 秒,并包含 config.txt:1。
- 文件不存在时不编造数字;重复调用最多执行 3 次。
- 三条轨迹的终止原因不同,每个工具结果均可对应一个 call_id。
合上书,检查一下
先作答,再看解释。答错的地方,正好是值得再读一遍的地方。
配合本章看视频
下面按本站章节主题选配。为什么没有采用原课录播 →
How I use LLMs ↗
CS2680 官网 9 月 9 日课表直接推荐。用模型交互的实际示范建立使用直觉,适合入门或进入上下文管理前观看;它不是 Agent 循环的逐行实现教程。
带着问题看 · 观察怎样向模型提供任务背景、资料与约束,再把一次对话和本站的多步工具循环作比较。
查看来源与观看说明 →Software Is Changing (Again) ↗
CS2680 官网 9 月 14 日课表直接推荐。提供理解 AI 软件和自主性取舍的背景,适合在讨论分工与人工介入前观看;并非多 Agent 框架实操课。
带着问题看 · 区分确定性软件与模型输出的工作方式;思考哪些步骤应自动执行、哪些应保留人工检查,再回到本章设计协作边界。
查看来源与观看说明 →Agent 的概念、原理与构建模式 —— 从零打造一个简化版的 Claude Code ↗
用 ReAct 与 Plan-and-Execute 展示 Agent 的控制流程,适合在实现本章循环之前建立直观认识。它是中文独立教程,不是 CS2680 录播。
带着问题看 · 作者时间轴:03:43 ReAct 运行流程;05:27 实现原理;10:49 动手实现;18:53 运行时序图。观看后对照本章补上终止条件、步数预算与错误处理。
查看来源与观看说明 →