第 1 课:Harness 与 Agent 运行时
这节课我们不讲复杂的数学公式,而是从一个你熟悉的场景开始:如何设计一个能持续执行任务、调用工具、处理错误,并且不轻易宣告完成的 Agent 系统。
先说明一下阅读范围:原书第 1 章的定义部分明确将 Agent 分成两部分------LLM 和 Harness。书中还将 Agent 的用途分为辅助人类,以及让 Agent 彼此交互两大类。我们这节主要学习前者,也就是工程上最常见的任务型 Agent。
一、先建立一个准确的认识
假设用户对你的 Agent 平台说:
请检查我的 Java 项目,修复所有导致测试失败的问题,并确保项目能够正常构建。
这个任务不是一次问答,而是一段持续的执行过程。
text
用户任务(目标、约束、验收标准)
↓
LLM:决策引擎(根据上下文决定下一步做什么)
↓
Harness:执行与控制系统
├─ 工具管理:调用与权限
├─ 状态与记忆:进度与上下文
├─ 执行循环:继续、重试、终止
└─ 安全与验证:审批与验收
↓
外部执行环境(文件系统、终端、测试工具、Git 等)
这里有一个很重要的细节:LLM 可以提出"运行测试"这个动作,但真正启动进程、检查退出码、收集日志的,应当是 Harness 中受控的执行代码。
这意味着,模型负责提出决策,程序负责落实决策并检查结果。模型可以参与验证,但不能让它仅凭自己的口头判断就决定任务已经成功。
二、Harness 究竟包含哪些东西?
我们先用一个工程化的划分来理解。下面是构建生产级 Agent 时实用的模块划分,不代表原书给出了完全相同的标准架构。
| 模块 | 核心职责 | 典型实现 |
|---|---|---|
| Execution Loop | 控制每一轮的执行和退出 | while 循环、状态机 |
| Model Adapter | 统一模型请求与响应 | OpenAI-compatible API、Responses API |
| Tool Registry | 注册、查找和调用工具 | 函数注册表、JSON Schema |
| Context Manager | 决定每轮向模型提供什么信息 | 历史裁剪、摘要、检索 |
| State Store | 保存任务进度和中间结果 | PostgreSQL、Redis、文件 |
| Policy & Hooks | 执行权限检查、审批和拦截 | 中间件、策略引擎 |
| Evaluator | 检查任务是否真正完成 | 测试、断言、业务验收 |
| Trace & Replay | 记录执行过程并支持复盘 | 结构化日志、事件记录 |
不必一开始就把每个模块都做成独立服务。个人开发者或小团队完全可以先在一个 Python 应用中实现它们。
三、最核心的部分:Agent 执行循环
这是 Harness 的心脏。
普通聊天程序的流程通常是:
text
用户输入 → 模型生成回答 → 返回结果
Agent 的流程则是:
text
接收任务
↓
准备上下文
↓
请求模型
↓
模型返回文本或工具调用
↓
检查并执行工具
↓
收集执行结果
↓
更新任务状态
↓
继续请求模型,或者验证是否完成
注意:每次工具调用完成,不代表整个任务完成。每次模型回答结束,也不一定代表整个任务完成。
1. 用伪代码理解核心机制
下面是一段刻意简化的 Python 伪代码。它展示的是控制流程,不是可直接运行的 SDK 示例。
python
async def run_agent(task):
state = create_task_state(task)
while not state.finished:
context = build_context(state)
response = await model.generate(context)
if response.has_tool_calls:
for call in response.tool_calls:
check_permissions(call)
result = await execute_tool(call)
state.add_observation(call, result)
persist_state(state)
continue
if response.is_final:
verdict = await evaluate_completion(state, response)
if verdict.passed:
state.finish(response.text)
else:
state.record_failure(verdict.reason)
state.request_replanning()
return state.result
这个版本还没有处理超时、并发工具调用、取消任务、崩溃恢复等生产问题,但足以让你理解 Harness 的主要职责。
2. 实际运行一遍
以修复 Java 项目为例:
第 1 轮:分析任务
- 模型决定先查看项目结构,调用
list_files。 - Harness 执行工具,并把文件列表交还给模型。
第 2 轮:执行测试
- 模型决定运行
mvn test。 - Harness 启动进程,保存退出码和测试报告。
第 3 轮:分析错误
- 测试报告显示一个空指针异常。模型决定修改相关代码。
- Harness 检查工具权限,执行文件修改,并记录变更。
第 4 轮:重新测试
- 测试仍然失败,模型根据新的错误日志继续修复。
- Harness 保留失败证据,并限制无效的重复尝试。
第 5 轮:验证完成
- 测试退出码为 0,测试报告通过,构建也成功。
- Harness 核对验收条件,满足后才将任务标记为成功。
这就是一个典型的 Observe → Decide → Act → Observe 循环。
它与 ReAct 等 Agent 方法在思想上是有关联的,但这里强调的是软件系统如何控制这个循环,而不是某一种特定的提示词格式。
四、一个容易忽略的区别:模型输出,不等于系统状态
这部分是从普通 Agent Demo 走向可靠 Agent 平台的关键。
假设模型返回:
我已经修复所有问题,测试全部通过。
你的系统能否直接把任务状态设置为 SUCCESS?
不能。这只是模型生成的一段文本,不是独立的成功证据。
Harness 应该区分三个东西:
Model Response:模型说了什么。Execution Result:工具实际上执行了什么。Task State:系统根据证据认定任务处于什么状态。
例如:
python
if test_exit_code == 0 and build_exit_code == 0:
task.status = "SUCCESS"
else:
task.status = "NEEDS_REPAIR"
这里仍需注意,测试通过只能证明测试覆盖范围内的行为符合预期。如果还要求特定文件被修改、接口契约保持不变或没有引入安全漏洞,就必须增加相应的验收条件。
工程原则:模型可以建议状态变化,但关键状态变化应当由确定性的程序规则和可验证证据决定。
这也是为什么生产级 Harness 不应仅仅是一个包装模型 API 的薄层。关于 Harness 的工程研究也强调了执行循环、工具治理、上下文管理、持久化状态与评估之间的协作。
五、Harness 应该如何设计状态机?
如果你只用一个 while 循环写完所有逻辑,初期确实简单,但随着任务变长,会逐渐遇到状态混乱的问题。
例如:
- 任务还没执行完,用户就关闭页面了。
- 服务重启后,任务从头开始执行。
- 模型调用超时,但上一次工具其实已经执行成功。
- Agent 卡在重复调用同一个工具。
- 用户要求停止任务,但执行中的子进程仍然在运行。
解决这些问题的第一步,是明确任务状态。
| 状态 | 含义 |
|---|---|
PENDING |
等待开始 |
RUNNING |
模型与工具执行中 |
WAITING_APPROVAL |
等待人工审批 |
VERIFYING |
检查验收条件 |
SUCCESS |
终态:任务成功完成 |
FAILED |
终态:任务失败 |
CANCELLED |
终态:任务被取消 |
text
PENDING ──▶ RUNNING ──┬──▶ WAITING_APPROVAL ──▶ RUNNING
├──▶ VERIFYING ──┬──▶ SUCCESS
│ └──▶ FAILED
└──▶ CANCELLED
示意状态机,实际状态转换应由你的业务规则定义。
1. 为什么要把状态持久化?
假设你正在执行一个 30 分钟的任务,已经修改了 20 个文件。
如果任务进度只保存在 Python 的局部变量里,一旦服务重启,状态就可能丢失。
建议至少保存:
| 字段 | 用途 |
|---|---|
task_id |
唯一任务标识 |
status |
当前生命周期状态 |
goal |
原始目标及约束 |
current_step |
当前执行阶段 |
step_count |
已执行轮数 |
tool_calls |
工具调用记录 |
last_observation |
最近的执行结果 |
acceptance_criteria |
验收条件 |
deadline |
任务截止时间 |
version |
并发更新控制 |
这并不意味着每轮都必须把所有信息重新塞进模型上下文。持久化状态和模型上下文是两回事:前者保存系统事实,后者只提供当前决策需要的信息。
2. 一个特别容易犯的错误:重复执行工具
假设 Agent 调用一个支付接口,接口已经成功扣款,但返回结果时网络断开了。
Harness 只看到超时,于是再次调用接口,就可能扣款两次。
因此,对有副作用的工具,应当考虑:
- 为每次业务操作生成幂等键。
- 记录调用是否已提交。
- 超时后先查询执行状态,而不是盲目重试。
- 将审批、授权和实际执行分开。
- 对无法确认结果的操作设置人工介入路径。
这也是 Harness 不只是"模型加几个函数"的原因。它必须处理真实软件系统中的失败语义。
六、结合你的 Agent 平台,应该如何落地?
你之前考虑过用 FastAPI 做管理平面,为每个用户提供隔离的虚拟计算环境,并允许 Agent 使用业务 API、Web 搜索、Python 函数和文件系统。这个场景可以采用下面的架构。
text
FastAPI:管理与任务入口(用户、会话、任务创建、鉴权、SSE 事件)
↓
Agent Runtime
├─ Loop Controller:执行与退出控制
├─ Model Adapter:模型切换与请求
├─ Tool Registry:工具注册、校验
└─ Context Manager:上下文构建与压缩
↓
持久化层:PostgreSQL(任务、状态、事件)
策略与验证:权限、审批、验收、预算
↓
每用户隔离的执行环境(文件系统、命令执行、浏览器、Python 沙箱)
这是一个建议的逻辑架构,不是必须拆分成多个微服务的部署架构。
我建议的技术选型
| 层次 | 初期方案 | 说明 |
|---|---|---|
| API 与管理 | FastAPI | 你可以集中开发管理接口和任务入口 |
| Runtime | Python 类与异步函数 | 先做模块化单体,不要过度抽象 |
| 状态存储 | PostgreSQL | 任务、执行事件、审批状态 |
| 模型接入 | 统一 Model Adapter | 屏蔽不同模型供应商的协议差异 |
| 工具执行 | Tool Registry + Policy | 统一参数验证、权限检查与结果格式 |
| 用户环境 | 隔离容器或沙箱 | 不要直接在主服务进程执行任意用户代码 |
| 实时事件 | SSE | 推送步骤、工具调用、错误和进度 |
| 知识检索 | 独立 RAG 模块 | 由 Runtime 按需调用 |
前期可以不引入独立消息队列、复杂多 Agent 编排系统或专门的记忆服务。等任务量、并发和可靠性需求明确后,再决定是否需要这些组件。
一个重要的架构边界
不要把业务任务状态、模型上下文和执行环境混成一个对象。
例如:
- 任务状态:当前是否运行、已完成哪些步骤、是否需要审批。
- 模型上下文:本轮推理需要看到的目标、历史摘要、工具结果。
- 执行环境:当前工作目录、文件、运行中的进程、依赖环境。
它们之间有关联,但生命周期不同。把它们分开,会让服务重启、上下文压缩、任务恢复和安全隔离更容易实现。
七、最值得记住的 5 个工程误区
-
误区 1:模型越强,系统就一定越可靠。 更强的模型可能减少决策错误,但无法自动解决工具超时、状态丢失、重复执行和权限失控。
-
误区 2:把最大轮数设置得越大越好。 轮数只是预算上限,不是任务成功率。还应限制时间、模型调用、工具调用、费用,并识别无进展循环。
-
误区 3:每轮都带上完整历史。 应按需构建上下文,并保留不可丢失的任务目标、验收条件和关键证据。
-
误区 4:所有工具都可以直接暴露给模型。 工具需要类型校验、权限控制、资源限制,以及对危险操作的审批和审计。
-
误区 5:任务有日志就足够了。 日志只能帮助你回看发生了什么。真正的评估还需要成功率、任务完成时间、成本、失败原因和可复现的测试。
八、最后做一个小练习:判断 Harness 应该如何处理任务
问题 1:模型声称"构建成功",但没有运行构建命令。系统应该怎么办?
- A. 直接标记成功,因为模型已经判断完成
- B. 要求提供构建证据,必要时执行构建验证
- C. 重新调用模型,让它再判断一次
问题 2:工具调用超时,但你不确定它是否已经执行成功。应该怎么办?
- A. 立即无限重试,直到拿到响应
- B. 查询执行状态,必要时通过幂等机制安全重试
- C. 告诉模型工具失败,让它随意选择下一步
进入下一课:实现一个可靠的 Agent Runtime
本课总结:Harness 不是简单的提示词包装器,而是让模型决策能够安全、持续、可恢复、可验证地转化为实际行动的软件系统。原书第 1 章提供了这个概念框架;你真正需要掌握的,是如何把它落实到执行循环、状态管理、工具治理和任务验收中。