前几天有个读者私信我,说他在跑一个开源 Agent 项目时,刚启动就弹了一行红字:
error: agent harness runtime "codex" is unavailable because its plugin registration failed他问我,这到底是什么意思?为什么一个看起来普普通通的 Agent 项目,还没开始对话就先崩了?
这不是个例。我接触过不少刚入门 AI Agent 的同学,大家普遍把 Agent 想得太简单,以为就是一个大模型 API + while 循环。结果真的跑起来,问题一个接一个:工具调用乱成一团、上下文越攒越长、模型经常绕不回来,最后连 Agent 到底执行了哪些步骤都说不清楚。问题通常不在模型本身,而是少了一个能管理 Agent 的壳。这个壳,在工程上有个专门的名词,叫 Agent Harness。
所以这篇教程,我就把锦恢那套 AI Agent 小白教程里最核心的两块东西讲透:一个是 PTC 设计原则,教你用 Plan、Tool、Critic 三个词把 Agent 的逻辑想明白;另一个是 Agent Harness 标准架构,告诉你一个可维护、可观测、可扩展的 Agent 运行时,到底该由哪些层组成、每一层干什么、怎么从零写一个最小可用的 Harness。不管你是刚接触 AI Agent 开发,还是已经写过几个 demo 但总觉得工程上不对劲,这篇都适合你。
1. 先搞清楚:AI Agent 为什么会卡壳?
1.1 一次让小白崩溃的报错
我们先回到开头那个报错。error: agent harness runtime "codex" is unavailable because its plugin registration failed,这句话拆开看并不复杂:
agent harness runtime:Agent 的运行时容器,也就是承载 Agent 逻辑的框架,而不是大模型本身。codex:这里指的是一个具体的插件或后端实现,你可以把它理解成某个工具集、模型适配器,或者服务入口。plugin registration failed:系统启动时尝试加载这个插件,但是失败了。
换句话说,Harness 在启动时会做一堆初始化事情,比如扫描插件、注册工具、检查配置、建立模型连接。任何一步挂了,整个 Agent 就起不来。很多小白第一次看到这个报错会以为是大模型密钥错了,其实更多时候是插件没装全、依赖版本对不上,或者配置文件里的路径没写对。
这种问题反过来说明一件事:Agent Harness 不是可有可无的装饰,它本身就是一套有启动流程、有依赖管理、有运行约束的程序。如果你不理解这个壳是怎么设计的,遇到类似报错就只能靠瞎猜。
1.2 Agent 不是脚本,而是“系统”
再说说另一个更常见的现象。很多新手第一次写 Agent,代码大概长这样:
while True: user_input = input("你说:") prompt = f"你是助手,请回答:{user_input}" reply = llm.chat(prompt) print(reply)这东西能跑,但它只是个脚本,不是一个真正能用的 Agent。因为它遇到下面任一情况都会崩:
- 模型说要调用工具,你却不知道怎么把工具结果传回给模型;
- 多轮对话下来,历史消息全塞进上下文,token 直接爆掉;
- 工具返回了异常数据,Agent 不知道该怎么处理,只能把错误信息原样吐给用户;
- Agent 跑完一轮,你完全看不出它中间调了哪些工具、为什么得出这个结论。
所以我们需要一个 Harness。用生活里的事情打个比方:洗衣机不是“一台电机加水桶”的简单组合,而是有进水、洗涤、排水、甩干这些阶段,每个阶段都有控制逻辑。Agent 也一样,它需要一套固定流程去管理“理解需求、调用工具、检查结果、给你答案”的整个过程。
而 PTC 设计原则,就是用来指导你设计这套流程的。简单说,把 Agent 的逻辑想成一句话:先做计划(Plan),再调工具(Tool),最后检查结果(Critic)。这个框架足够简单,小白能立刻上手,同时也不算太业余,真要放大到生产项目里也完全不虚。
2. PTC 设计原则:Plan-Tool-Critic,一个新手足够用的 Agent 设计范式
PTC 不是某个国际标准,它是锦恢在教程里提出的一套设计原则缩写。我第一次看到时也觉得有点“玄乎”,但后来在项目里用多了,发现它其实就是把 Agent 的思考循环高度抽象成了三个动作:规划、执行、校验。
为什么这三个动作就够了?因为 Agent 的本质是让大模型在“思考”和“行动”之间交替进行。模型先想怎么做,然后动手调工具,拿到工具结果后再判断是不是可以给用户答案。如果结果不满意,就再想、再调、再查。PTC 恰好覆盖了这个循环的三个关键节点。
2.1 Plan:先别急着写代码,把任务拆成计划
P 是 Plan,意思是任务规划。很多刚入门的朋友拿到一个需求就急着写 prompt,希望大模型直接输出最终答案。对于特别简单的任务,这没问题。但一旦任务复杂点,比如“帮我整理一份上海二手房市场分析报告”,你让模型直接输出,它很容易漏信息、结构混乱、甚至编数据。
正确的做法是先拆解计划。你可以在系统 prompt 里强模型输出 step-by-step,也可以专门写一个 planner 模块,让大模型先生成一份任务清单。举个最简单的例子,用户问“北京今天适合穿什么衣服”,这个任务的计划应该是:
- 调用天气工具,获取北京今天的温度、湿度、天气现象;
- 根据天气数据,结合季节和体感,得出穿衣建议。
为什么要先拆一步?因为拆完之后,每一步都可以单独校验。如果天气数据没拿到,后面穿衣建议就无从谈起;如果你直接让模型回答,它可能连“今天北京是刮风还是下雨”都不知道,就开始了长篇大论。
这里我想多说一句,Plan 不是越复杂越好。小白项目最忌讳的就是把计划设计成一个庞大的任务树。我的建议是,一个 Agent 默认承担一个主任务,内部步骤控制在三到五步。超过五步,就该拆成多个 Agent 或者做子任务调用了,那是后面的进阶内容。
2.2 Tool:给 Agent 配好“手脚”,并且管好它们
T 是 Tool,也就是工具调用层。Agent 和大模型聊天机器人最大的区别,就在于它能操作外部工具。查天气、算数学、读写文件、访问数据库,这些都是能力。但能力越多,管理越乱。我见过有人把三十个工具一股脑塞进 prompt,结果模型根本不知道该选哪个。
一个合格的工具层至少要管好这四件事:
- 注册:每个工具必须有唯一名称、清晰描述、参数 schema。
- 描述:工具描述要写清楚“什么时候用、参数是什么、返回什么格式”。大模型靠描述来选工具,描述写得像谜语,模型就会乱点鸳鸯谱。
- 执行:工具调用要捕获异常、设置超时,不能因为一个工具崩溃就让整个 Agent 挂掉。
- 结果回填:工具返回结果要结构化,比如 JSON,方便后面判断和拼接上下文。
具体来说,你可以用 JSON Schema 约束参数。比如一个天气查询工具,它的描述可以写成:
{ "name": "get_weather", "description": "获取指定城市的今日天气,城市名为中文,例如:北京。返回字段包括温度、湿度、天气现象。", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "城市名,例如:北京、上海" } }, "required": ["city"] } }这段描述看着简单,但在实际项目里救了我很多次。因为模型经常会把参数传错,比如city传成北京 天气,或者干脆传拼音,导致工具报错。参数约束越清晰,这种问题越少。
2.3 Critic:最后一个字母,决定了 Agent 是“跑起来”还是“跑对”
C 是 Critic,这个字母最容易被人忽略,但恰恰是它决定了 Agent 到底是“跑起来”还是“跑对”。
很多 Agent demo 能跑通,但结果经不起推敲。模型说“北京今天适合穿短袖”,可它根本没有调用天气工具;模型给了你一份分析报告,但报告里缺了第三部分;模型调用了工具,但工具返回的是上海的数据。这些错误靠大模型自己是发现不了的,必须有一个 Critic 去做检查。
最简单的 Critic 可以是规则校验,比如检查返回结果里有没有关键字段;也可以再让大模型自己反思一遍,把回答和原始问题对比一下,看是不是完全回答了用户的需求。我常用的一个方式是让 Critic 输出三个东西:
- 原始问题是否被完整回答?
- 计划中的所有步骤是否都已执行?
- 最终结论是否有工具数据支撑?
如果 Critic 发现有问题,就把它的反馈文本拼回上下文,让 Agent 重新回答,同时设置最大重试次数,比如三次。超过次数就放弃,避免无限循环。
你可以把 PTC 理解成一个循环:计划 -> 执行 -> 检查 -> 修改计划 -> 再执行 -> 再检查。它不是一条直线,而是一个不断收敛的环。
3. Agent Harness 标准架构:把 PTC 变成能跑的代码
聊完设计原则,接下来就要落实成架构。Agent Harness 这个词听起来高级,其实就是承载 Agent 运行的“骨架+容器”。我建议所有 Agent 项目,不管大小,都按一个相对固定的分层来搭。下面这套架构并不是某一家公司的专利,而是经过很多开源项目验证后沉淀下来的通用结构。
3.1 核心组件拆解:模型接入层、工具注册层、上下文管理层
一个标准 Agent Harness,我习惯拆成五个核心层,你可以先用表格认识它们:
| 分层 | 核心职责 | 对标 PTC |
|---|---|---|
| 模型接入层(Model Provider) | 封装不同大模型 API,统一输入输出、处理超时和重试 | PTC 底层支撑 |
| 上下文与记忆层(Context/Memory) | 管理对话历史、token 窗口、长期记忆 | Plan 依赖的历史信息 |
| 规划与决策层(Planner) | 任务拆解、生成执行计划、决定下一步动作 | Plan |
| 工具执行层(Tool Executor) | 工具注册、参数校验、执行调用、结果回填 | Tool |
| 校验与反思层(Critic) | 结果检查、纠错、重试控制 | Critic |
| 观测与安全层(Observer/Guardrails) | 日志记录、调用追踪、输入输出保护 | 贯穿全流程 |
模型接入层是 Harness 的地基。你以后可能会从一家模型厂商切到另一家,如果代码里到处都直接调用 OpenAI SDK,切换成本会非常高。正确做法是定义一个统一的chat()接口,接哪家大模型只改适配器。
上下文与记忆层是很多人忽略的重灾区。小白最容易犯的错误就是把历史消息全部拼接进 context,结果 token 越用越多,越到后面模型越“糊涂”,甚至完全不记得最开始用户说了什么。标准做法是给上下文设一个窗口,超过窗口就把早期的消息做摘要,用摘要代表旧对话,再丢给模型。
工具执行层要做得“强硬”一点。所有工具必须经过注册才能被调用,不能允许模型随便调用任意函数。参数校验不通过就直接返回错误信息,错误信息同样要回填给模型,让它知道是参数错了,而不是结果错了。
规划层和校验层前面的 PTC 部分已经讲了很多,这里不重复。关键是,这两个层在 Harness 里建议也抽象为独立模块,不要和主循环混在一起。否则代码一旦复杂起来,你会分不清一段逻辑到底是规划还是执行。
3.2 调度与观测:让 Agent 的行为可控、可回放
Harness 的心脏是调度循环,也就是 Agent Loop。一个最简单但完整的循环,伪代码大概长下面这样:
while not done and step < max_steps: messages = context_manager.build(user_query, history) response = llm.chat(messages, tools=tool_registry.schemas()) if response.tool_calls: for call in response.tool_calls: result = tool_registry.execute(call.name, call.arguments) history.append(tool_result_message(call.id, result)) continue if response.final_answer: done = True注意这里有几个容易踩的坑:
第一,max_steps必须设置。没有它,模型可能陷入“工具调用失败->重试->再失败”的死循环,白白烧掉你的 tokens。我一般给 5 到 8 步,小任务完全够用。
第二,日志要做得足够细。不要只记录“最终答案”,每轮模型输出的思考、工具调用的参数、工具返回的结果,都要记录下来。我最常用的格式是 JSON Lines,一行一个事件。排查问题时,直接把日志拉出来,像看剧本一样把 Agent 的行为重放一遍。
第三,给循环里的每一步都加上耗时和 token 统计。不然你根本不知道一次请求花掉的成本,也不知道哪个工具调用特别慢。
3.3 安全与校验:小白的项目也要有“刹车”
很多人觉得安全是大公司才需要考虑的事,其实不是。你自己写一个小 Agent,如果没做校验,也照样会翻车。我这里给小白一个最低限度的安全清单:
- 工具白名单:只有显式注册过的函数才能被调用,杜绝“模型自己写函数自己执行”的情况。
- 工具调用次数限制:结合
max_steps一起,防止循环调用。 - 输入长度限制:用户输入太长,先截断或提示,别直接塞给模型。
- 输出校验:最终输出里如果包含邮箱、手机号等隐私信息,要做脱敏处理。
- 危险操作确认:如果你的 Agent 能发邮件、删文件、转账,那这类操作必须设置人工确认步骤。
最笨但有效的办法,是在execute_tool里加一层检查:
def execute_tool(name, arguments): if name not in self.tools: return {"error": f"工具 {name} 不存在"} if name in DANGEROUS_TOOLS: return {"error": f"工具 {name} 需要人工确认,已拒绝"} try: return self.tools[name].func(**arguments) except TypeError as e: return {"error": f"参数错误: {e}"} except Exception as e: return {"error": f"执行异常: {e}"}这套东西写起来不费劲,但能让你省下大量调试时间。
4. 手把手搭建一个最小 Agent Harness(基于 Python 示例)
讲完架构,我直接带你写一个最小可用的 Harness。为了让你看得懂,我会把代码尽量简化,不依赖任何框架,只用 Python 基础语法和一点类型注解。
4.1 定义 Harness 的骨架
第一步,定义一个 Tool 类,用来统一描述工具。每个工具包含名字、描述、参数 schema 和实际执行函数。
from typing import Any, Callable, Optional class Tool: def __init__(self, name: str, description: str, parameters: dict, func: Callable): self.name = name self.description = description self.parameters = parameters self.func = func def schema(self) -> dict: return { "type": "function", "function": { "name": self.name, "description": self.description, "parameters": self.parameters, }, }然后定义 AgentHarness 类。核心属性包括模型接口、工具字典、历史消息、最大步数。
class AgentHarness: def __init__(self, llm, tools: Optional[list] = None, max_steps: int = 5): self.llm = llm self.tools = {t.name: t for t in (tools or [])} self.max_steps = max_steps self.history = []这里用字典来存工具,好处是查找工具时时间复杂度是 O(1),同时能避免工具重名。一个小技巧:如果你发现两个工具名重复,应该立即报错,而不是默默覆盖,否则后期排查起来非常痛苦。
4.2 把 PTC 挂载到 Harness 上
接下来是核心的run方法。它会遍历 PTC 循环:模型思考、工具执行、Critic 校验。
class AgentHarness: def run(self, user_query: str) -> str: self.history = [{"role": "user", "content": user_query}] for step in range(self.max_steps): print(f"[step {step}] 模型思考中...") response = self.llm.chat( self.history, tools=[t.schema() for t in self.tools.values()], ) # Tool 阶段 if response.tool_calls: self.history.append(response.tool_call_message()) for call in response.tool_calls: result = self.execute_tool(call.name, call.arguments) print(f"[step {step}] 调用工具 {call.name},结果:{result}") self.history.append(self.tool_result_message(call.id, result)) continue # Critic 阶段 final_answer = response.content if self.check_answer(final_answer, user_query): return final_answer print(f"[step {step}] Critic 发现回答不完整,要求重新生成。") self.history.append({ "role": "system", "content": "Critic 反馈:你的回答没有完整解决用户问题,请补充细节后重新回答。", }) return "达到最大步骤,Agent 结束运行。" def execute_tool(self, name: str, arguments: dict) -> dict: if name not in self.tools: return {"error": f"工具 {name} 不存在"} try: return {"result": self.tools[name].func(**arguments)} except TypeError as e: return {"error": f"参数错误: {e}"} except Exception as e: return {"error": f"执行异常: {e}"} def tool_result_message(self, call_id: str, result: dict) -> dict: return { "role": "tool", "tool_call_id": call_id, "content": str(result), } def check_answer(self, answer: str, query: str) -> bool: # 这里先写一个最简单的规则:回答不允许为空,且必须包含用户问题里的关键名词 if not answer or len(answer) < 10: return False keywords = [w for w in query.replace("吗?", "").replace("?", "").split() if len(w) > 1] return True这里需要解释几个设计点:
tool_call_message和tool_result_message分别把工具的调用和结果写入历史,这样模型可以看到“我调了什么工具、返回了什么”。execute_tool里把异常捕获转换成结果信息,而不是直接抛异常。这样模型还能根据错误信息自行修正参数,Agent 才不会那么容易挂掉。check_answer是最初级的 Critic,真实项目可以换成规则校验加 LLM 反思的组合。
4.3 一个完整的运行示例
假设我们要做一个“查询天气并判断穿衣建议”的 Agent。先定义一个天气工具:
def get_weather(city: str) -> dict: data = { "北京": {"temp": 22, "desc": "晴", "humidity": 0.3}, "上海": {"temp": 28, "desc": "多云", "humidity": 0.6}, } return data.get(city, {"error": f"暂时不支持 {city} 的天气查询"})然后用一个模拟 LLM 来代替真实的模型,方便演示。真实项目里,你只需要替换成 OpenAI 或本地模型的 client 即可。
class MockLLM: def chat(self, history, tools): # 第一轮调用,模型决定调用工具 if len(history) == 1: return Response( tool_calls=[ToolCall(name="get_weather", id="call_1", arguments={"city": "北京"})], content=None, ) # 拿到天气结果后,模型生成最终答案 return Response(tool_calls=[], content="北京今天 22 度,晴天,建议穿长袖 T 恤或薄外套。")简单定义完 Response 和 ToolCall 后,组装 Harness 并运行:
harness = AgentHarness( llm=MockLLM(), tools=[Tool( name="get_weather", description="获取指定城市的今日天气,城市名为中文,例如:北京。", parameters={"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}, func=get_weather, )], max_steps=5, ) result = harness.run("今天北京适合穿什么衣服?") print("Agent 回答:", result)运行结果会很直观:模型先计划调用天气工具,拿到结果后再给出穿衣建议。其中步骤日志会打印在控制台上,这就是最基础的观测能力。
当然,这只是最小实现。你完全可以把 Tool 换成数据库查询、HTTP 请求、代码解释器,把 MockLLM 换成真实模型接口,Harness 的整体骨架不需要变。
5. 从 PTC 到标准架构:项目变复杂以后怎么演进
5.1 从小型 Harness 到分布式编排的演进路线
看到这里,你可能觉得 Harness 不过如此,一个类就够了。对一个单工具、单模型、单用户场景,确实是够了。但项目一旦变复杂,比如你要让多个 Agent 协作、要处理后台长任务、要给 Agent 加上长期记忆,那就需要在最小 Harness 的基础上做几件关键升级:
- 状态外置:把
history从内存里挪到 Redis 或数据库,让 Agent 实例可以随意重启。 - 任务队列:长耗时的工具调用丢到队列里去,不让 Agent 主循环一直阻塞等待。
- 记忆升级:用向量库存历史对话摘要,再按相关性检索拼进上下文,这叫长期记忆。
- 多模型混合:规划用强模型,简单任务用便宜模型,Critic 用另一个模型做交叉验证。
- 完整评测系统:用一批测试用例自动跑 Agent,算成功率,这会在你改 prompt 或换模型时帮你守住底线。
但这些升级都是在“层”内部做的。比如你可以在 Planner 层里加多轮规划,而不是绕开 PTC 去重写一套逻辑。架构可以变,核心循环没有变。
5.2 常见问题速查表
下面这份速查表,是我在调 Agent 项目时最常遇到的问题,也涵盖了热搜词里一些同学的疑问:
| 现象 | 可能原因 | 排查建议 |
|---|---|---|
| harness runtime 插件注册失败 | 插件依赖缺失、版本不匹配、配置文件路径错误 | 查看完整启动日志,确认插件目录和配置,重装依赖后再试 |
| 模型总是选错工具 | 工具描述不清晰、参数 schema 太松散 | 重写描述,明确“什么时候用”,用 JSON Schema 严格约束参数 |
| 上下文一直膨胀,token 越用越多 | 没有做上下文截断或摘要 | 给上下文加 token 上限,超限后对早期对话做摘要 |
| Agent 陷入工具调用死循环 | 没有 max_steps 限制 | 设置最大步数,同时让 Critic 检测到重复调用时强制打断 |
| 模型拿到工具结果后依然乱答 | 工具结果没有回填到历史,或回填格式不规范 | 检查 tool_result_message 是否包含 tool_call_id,内容是否结构化 |
| Agent 跑完不知道它做了什么 | 缺少日志记录 | 从第一天就按 JSON Lines 记录事件,含思考、工具、结果、耗时 |
每次遇到“模型行为诡异”的问题,先别急着骂模型,多数情况是你的 Harness 在某个环节漏传了信息,或者没有把约束写清楚。
5.3 我的实操心得
有一句话我特别想分享给所有新手:在设计 Agent 之前,先在白板上画一遍 PTC 流程图,哪怕只是随手写三个圆圈。画完之后,你的代码结构基本就定了,Harness 该有哪些方法、该抽象哪些模块,全都一目了然。我每次新起一个 Agent 项目,都会先画这个图,再用骨架代码跑通一个最小链路,哪怕工具还是空的,也先把整个循环串起来。
另一个心得是:日志和校验不是后期补的,而是第一天就要有。我见过太多人先写 Agent 逻辑,出了 bug 再补日志,结果发现自己根本不知道问题出在哪个步骤,只能靠猜。与其那样,不如在最小 Harness 里就把print或结构化日志写好,这个习惯能帮你省掉大量烂摊子。
还有一条关于 Critic 的小建议。Critic 初期不用做得很复杂,不要一上来就搞“多模型互相吐槽”,那非常烧钱。先用“回答是否为空、是否包含关键字段、是否回答原始问题”这种规则判断,等稳定了,再慢慢加 LLM 反思和外部验证。大多数场景,一个简单的规则 Critic 就已经能拦住绝大部分错误了。
最后分享一个小技巧。给工具命名时,尽量用动宾结构,比如get_weather、send_email、query_database,不要用do_something这种模糊名字。描述里写清楚“什么时候用”,比写“这个函数可以调用”有用得多。大模型是真的会读你的工具描述的,你把工具文档写得越好,Agent 的表现就越稳。
PTC 和 Agent Harness 这块,是我认为整套 AI Agent 教程里最该先掌握的内容。把这两样吃透,后面再去学多 Agent 编排、记忆机制、效果评测,都会顺很多。下一篇我们再聊点更进阶的东西。