上个月帮一个朋友 review 他写的 LLM Agent 项目,代码里塞了三十多个 if-else 来切换工具调用、角色、上下文拼接。看着像一张摊开的意大利面。问题不是模型不行,而是整个执行过程没有一个像样的Agent Harness。你只有 Agent,没有让 Agent 运转起来的“骨架”。这篇文章我从一个后厨类比的视角,把手里的 Agent Harness 拆成 12 个组件,顺便解释每个组件为什么存在、怎么用、哪些可以一开始就砍掉。适合正在设计多步骤 AI Agent、或者想把一堆 prompt 组织成工程化系统的开发者看。
1. 先把概念拎清:Agent 是演员,Harness 是舞台
很多人在网上搜“harness 和 agent 区别”,搜完还是懵。我直接用一句大白话总结:Agent 是那个负责“思考”的演员,Harness 是整个舞台的机械系统。演员台词再好,没有灯光、提词器、后台调度、安全网,也没法完成一出完整的话剧。Agent Harness 就是承载 Agent 运行的一套外围框架,负责接收任务、编排流程、调用工具、管理记忆、记录日志、控制权限。
从实现上讲,Agent 是一个循环:把用户请求拼进 prompt,调大模型,拿结果,判断是否需要调用工具,再继续调模型。而 Agent Harness 是这个循环之外的“操作环境”。
| 对比项 | Agent | Agent Harness |
|---|---|---|
| 核心职责 | 推理、决策、生成回复 | 编排、状态、工具、通信、治理 |
| 控制粒度 | 单个循环内的输入输出 | 跨步骤、跨多 Agent 的流程 |
| 典型问题 | 怎么让模型“想”得更准 | 怎么让系统“跑”得更稳 |
| 实现产物 | prompt + 模型 API 调用 | 调度器、记忆库、事件总线、观测组件 |
| 单独使用效果 | 能完成简单单步任务 | 能承载复杂、可恢复、可观测的 Agent 应用 |
所以当你说“我做了个 Agent”,你大概率只是写了 prompt 和模型调用;当你说“我搞了一个 Agent Harness”,你是在定义这个 Agent 如何被创建、执行、暂停、恢复、追踪和治理。后者才是工程,前者是实验。
一个反直觉的事实是:调试复杂 Agent 时,瓶颈往往不在大模型的“智商”,而在 Harness 的“组织能力”。任务解析含糊、工具调用结果没有归档、断点续跑做不到、日志里看不见中间步骤——这些问题用再强的模型也补不回来。
2. 用一个“晚宴后厨”的类比,从 12 个问题倒推出 12 个组件
如果说一个复杂的 Agent 系统是一场晚宴,那么 Agent Harness 就是后厨。顾客不会看到厨师怎么备料、怎么协调、怎么应急,但后厨的每个环节直接决定菜能不能稳定端上去。
我不主张凭空列组件清单,而是先列出“运行一个复杂的 Agent 时必须回答的 12 个问题”,再把问题映射到后厨角色,最后抽象成组件。
- 用户到底让我干什么?
- 先干什么、后干什么?
- 谁来实际动手干?
- 用什么口径、格式、限制去干?
- 不会的事情怎么调用外部能力?
- 干到一半,上一步的细节忘了怎么办?
- 下次还想用上次沉淀的信息怎么办?
- 多角色/多步骤之间怎么传话?
- 进行到哪一步了,中断后能不能接着干?
- 结果靠不靠谱,要不要校验?
- 出了问题,怎么定位是哪一步错了?
- 是不是什么操作都允许 Agent 执行?
对应到后厨,就是:菜单、总厨、灶台、菜谱、备料台、手边冰箱、档案室、对讲机、台账本、试菜员、监控屏、保险柜。再抽象成工程组件,正好是下面这张映射表。
| 后厨类比 | 要解决的工程问题 | Agent Harness 组件 |
|---|---|---|
| 菜单/下单台 | 用户意图转结构化任务 | Task Parser 任务解析器 |
| 总厨 | 决定做菜顺序和派工 | Orchestrator 编排调度器 |
| 灶台 | 真正执行的厨师 | Agent Runtime/Executor |
| 菜谱 | 控制行为范式的描述 | Prompt Manager 提示词管理器 |
| 备料台 | 外部技能和函数调用 | Tool Invoker 工具调用器 |
| 手边冰箱 | 当前对话上下文 | Context Manager 上下文管理器 |
| 档案室 | 跨会话的长期记忆 | Long-term Memory 长期记忆库 |
| 对讲机 | 组件间通信 | Event Bus 事件总线 |
| 台账本 | 状态和断点保存 | State Store 状态存储 |
| 试菜员 | 输出校验与兜底 | Guardrails 护栏组件 |
| 监控屏 | 日志、追踪、指标 | Observability 可观测组件 |
| 保险柜 | 权限、密钥、操作边界 | Security/Auth 安全组件 |
这 12 个问题不是拍脑袋,它们覆盖了 Agent 运行链路的四个平面:决策、执行、支撑、治理。每层 3 个组件,形成了紧凑的 3×4 结构。后厨类比的妙处在于:它天然把“流程协作”“资源管理”“异常兜底”这三个要素放在了一起,这正好是 Agent Harness 的核心命题。
3. 12 个组件逐个拆解:职责、接口和关键点
组件清单列出来了,但每个组件到底做什么、接口长什么样、什么时候必须做,这些决定你能不能把理论落地。我按四个平面展开讲。
3.1 决策链路:任务解析、编排调度、提示词管理
这一层负责告诉系统“用户要什么、按什么顺序做、用什么话术做”,是整个 Harness 的头部。
Task Parser 任务解析器,对应后厨的菜单。它不是简单的意图分类器,而是要把“帮我查一下附近有没有卖情人节玫瑰的店,顺便规划一条路线”解析成结构化的任务:intent=purchase_search, targets=["flower_shop", "route"], constraints=["valentine", "nearby"]。常见实现有两种:一种是直接让大模型输出 JSON,一种是结合规则做槽位抽取。我建议先让模型自由解析,再用 schema 校验,把不合格的重试一次。接口可以直接抽象成def parse(user_input: str) -> Task。
Orchestrator 编排调度器,对应总厨。它决定子任务按什么顺序执行,能不能并行,失败怎么重试。这里要区分两种风格:中心化编排和 ReAct 式自循环。中心化编排适合步骤明确的任务,比如“先搜索,再总结,再发邮件”;ReAct 自循环适合动态探索,比如“持续检索直到找到答案”。一个成熟的 Harness 通常两者都要支持,用有限状态机来描述步骤流转,而不是靠模型自由发挥。接口大概是def run(task: Task, context: Context) -> Result。
Prompt Manager 提示词管理器,对应菜谱。没有它,你会把 prompt 拼在代码里,换一个模型就崩。这个组件负责统一管理系统提示词、few-shot 示例、工具描述模板,并支持按组件或按任务类型渲染。关键点是变量隔离:只有通过render(task, context)才能注入动态内容,而不是到处用 f-string 直接拼接。否则 prompt 会变得不可测试、不可审计,出问题的时候根本分不清是模型问题还是模板问题。
这一层最容易犯的错是让 Orchestrator 直接改 prompt。编排器只应该决定流程走向,不该关心措辞。prompt 是 Prompt Manager 的地盘。
3.2 执行链路:Agent 运行器、工具调用器、上下文管理器
任务定好了,接下来要有人动手。这一层是离大模型最近的地方。
Agent Runtime/Executor,对应灶台上的厨师,也是很多人理解的“Agent”。它维护最内层的推理循环:把当前消息发给模型、解析模型输出、检测是否有工具调用、如果有就触发工具、把工具结果放回对话、再继续。要注意它和 Orchestrator 的区别:Orchestrator 管的是“多个步骤/多个 Agent 之间的宏观编排”,Agent Runtime 管的是“单个 Agent 内部的模型-工具循环”。接口核心是def step(messages: list) -> AgentUpdate,每次只走一步,把控制权交还给 Harness。这样你才能在外面做中断、暂停、人工审批。
Tool Invoker 工具调用器,对应备料台。真实项目里工具签名五花八门,有些是 REST API,有些是 Python 函数,有些是数据库查询。Tool Invoker 要解决三件事:工具发现(有哪些工具可用)、参数转换(把模型给的 JSON 转成实际函数参数)、结果归一化(把返回结果统一包装成模型能理解的消息)。最容易被忽略的是错误透出。工具抛异常时,不能直接把堆栈丢给模型,而是要把“发生了什么、有什么替代方案”变成结构化的工具反馈。比如搜索超时,应该返回{"status": "timeout", "suggestion": "try_again"}。
Context Manager 上下文管理器,对应手边冰箱。所有需要进入模型上下文的信息都在这里汇聚,同时它也是 token 预算的守护者。需要考虑滚动窗口、自动摘要、丢弃不重要信息。很多人问“上下文管理器跟记忆组件有什么区别”,区别很简单:上下文管理器管的是“这一次推理要带什么”,长期记忆管的是“以后还能不能找回”。接口可以设计成def build_system_messages(task, state) -> list和def compact(messages) -> messages。
执行链路是出 bug 最多的地方。我见过最常见的 bug 是工具调用结果过长导致上下文爆炸,模型开始重复已经说过的内容。解决方案不是调大 token 上限,而是让 Context Manager 学习摘要压缩。
3.3 支撑链路:事件总线、状态存储、长期记忆
这三兄弟不直接参与推理,但少了它们,Harness 只能跑单机单线程,连中途续跑都做不到。
Event Bus 事件总线,对应后厨的对讲机。组件间需要解耦通信,比如工具执行完发一个ToolFinished事件,编排器监听后决定下一步,观测组件同时监听来做日志记录。实现上可以用一个简单的发布订阅类,不需要一上来就上消息队列。事件应当是不可变的,包含时间戳、事件类型、关联的run_id。比如:
@dataclass class Event: type: str # "tool.finished", "agent.step", "task.pending" run_id: str payload: dict timestamp: floatState Store 状态存储,对应台账本。Agent 每执行一步,都要把状态持久化:当前在哪个节点、已经收集了哪些信息、哪些工具调用成功过、有没有待人工确认的东西。为什么需要单独的组件?因为内存中的对象一旦进程重启就全没了。生产级 Harness 至少要把 state 写入 Redis 或 Postgres。接口就两个:save(run_id, state)和load(run_id)。有了它,你才能实现“任务中断后恢复”,而不是从头再来。
Long-term Memory 长期记忆库,对应档案室。这里存的是结构化知识、历史会话摘要、用户偏好。常见的实现是向量库(存语义检索的 chunk)加关系表(存事实)。不要把它和 Context Manager 搞混:长期记忆不是每轮都全文塞给模型,而是先通过检索把相关片段取出来,交给 Context Manager 做组装。长期记忆的录入也必须有策略,不能把乱七八糟的内容都写入,要有评分、去重和过期机制。
这一层设计好了,Agent 才是真正的“可规模化系统”。否则换个节点就跑不了,改个参数就丢上下文。
3.4 治理链路:护栏、可观测性、安全权限
执行完后厨还要有人把关。没有治理层的 Harness 只能跑 Demo,不能跑业务。
Guardrails 护栏组件,对应试菜员。它在 Agent 的输入侧和输出侧做双向检查。输入侧防止用户注入恶意 prompt;输出侧校验结果格式、敏感信息、是否符合规范。注意护栏不是简单字符串过滤,它还可以做结构化验证:比如要求 Agent 输出 JSON,护栏先尝试解析 JSON,解析失败就让模型修正一次。也能检查工具调用参数是否在白名单内,防止 Agent 去调用删除类操作。这个组件放在 Agent Runtime 的外围,作为每个 step 之后的拦截点。
Observability 可观测组件,对应监控屏。复杂 Agent 的调试难度是几何级上升的,没有 trace 的话,你根本不知道是模型回答错了,还是工具返回错了,还是编排顺序错了。这个组件至少要记录:每次模型请求的 prompt 摘要、返回 token 数、耗时、工具调用的输入输出、各步骤的事件流。如果团队有条件,把事件总线里的所有事件都接到这个组件,就能还原一次完整运行的“时间线”。这一步做到位,线上问题处理效率能提升一个量级。
Security/Auth 安全组件,对应保险柜。包括密钥管理、用户身份校验、数据隔离、操作审计。很多人觉得“我就是个内部工具,不用管”,但一旦 Agent 可以调工具,它就有了实际影响力。必须做到:所有外部工具调用的凭证不过明文,工具执行前校验权限,关键操作(发邮件、删文件、改数据库)进入审计日志。安全组件要直接嵌入 Tool Invoker 和 API 入口,不能是事后补丁。
治理链路不是可选项。我见过太多项目立项时觉得麻烦,结果一上生产就出事:要么工具被 prompt 注入控制,要么出了问题完全无从查起。
4. 从后厨回到代码:一个 12 组件 Agent Harness 的最小骨架
光有概念不成,我给出一个可以直接参考的伪代码骨架。这不是某个框架的完整实现,而是告诉你 12 个组件如何咬合在一起。
先定义一个最小配置:
harness: components: task_parser: enabled: true max_retries: 1 orchestrator: strategy: fsm max_steps: 10 agent_runtime: model: chat-llm temperature: 0.2 tool_invoker: timeout_seconds: 15 retry: 1 context_manager: max_tokens: 4000 strategy: sliding_window long_term_memory: store: vector_store event_bus: enabled: true state_store: backend: redis guardrails: verification: json_schema observability: traces: true security: requires_auth: true然后是运行时的主类,核心思路是让每个组件只做一件事,由 Harness 主循环把它们串起来:
class AgentHarness: def __init__(self, components): self.parser = components["task_parser"] self.orchestrator = components["orchestrator"] self.executor = components["agent_runtime"] self.tools = components["tool_invoker"] self.context = components["context_manager"] self.state = components["state_store"] self.events = components["event_bus"] self.guardrails = components["guardrails"] def run(self, user_input, run_id=None): run_id = run_id or create_run_id() task = self.parser.parse(user_input) plan = self.orchestrator.plan(task) self.state.save(run_id, {"plan": plan, "step": 0, "context": {}}) self.events.publish(Event("harness.start", run_id, {"task": task})) while not plan.is_finished(): step = plan.next_step() messages = self.context.build(step, self.state.load_context(run_id)) result = self.executor.execute(step, messages, self.tools) validated = self.guardrails.validate(result) if not validated.ok: result = self.executor.repair(result, validated.feedback) self.state.update(run_id, step, result) self.events.publish(Event("harness.step", run_id, {"step": step, "result": result})) plan.advance() self.events.publish(Event("harness.end", run_id, {})) return self.state.load(run_id).final_answer一次请求的完整流转是这样的:用户输入进入 Harness,Task Parser 先翻译成结构化任务,Orchestrator 生成步骤计划,State Store 马上落账。然后进入循环,每一步先从长期记忆检索相关信息,交到 Context Manager 组装消息,Agent Runtime 调用模型,模型决定要用哪个工具,Tool Invoker 执行并把结果写回上下文,Guardrails 检查结果,状态更新,事件广播,直到计划完成。
这个骨架不是炫技,它体现了 Harness 的精髓:每个组件都位于固定的边界内,流程可暂停、可恢复、可追踪。在你自己的实现里,可以按需替换组件实现,比如把 Redis 换成内存,事件总线换成最简单的函数回调,只要保持接口稳定就行。
5. 我实际落地时的三个教训:组件不是越多越好
上面的 12 个组件是“完全体”,但真实项目里你不需要一上来就全部铺开。我踩过几个坑,说出来供你参考。
第一个教训:别为了用组件而拆组件。最初给一个内部运维工具做 Harness,我强行拆了 12 个组件,结果一大半时间花在定义接口和搬数据上。后来冷静下来,只保留 Task Parser、Orchestrator、Tool Invoker、State Store,其他都砍掉,开发速度快了三倍。如果任务只有三五个步骤,执行链路单一,那先砍掉长期记忆和事件总线,用函数的返回值传递结果就够了。加组件应该基于真实的失败模式,而不是基于架构理想。先让 Agent 跑起来,再根据痛点补组件。
第二个教训:State Store 和 Observability 最容易偷懒,但最不能偷懒。我第一次做完一个多步骤 Agent 后,没有做持久化,结果线上进程一重启,所有正在执行的任务全丢了。后来补了 Redis 状态存储,才发现恢复任务比自己想的重要。Observability 也是,以前以为打印点日志就行,直到一次线上事故无法还原发生了什么。现在我会在 Event Bus 发出所有事件时同步给观测组件,至少把run_id、步骤、关键 Prompt、工具返回记录完整。
第三个教训:事件总线不要往消息队列的方向做。你要是把每个事件都丢进 Kafka 再做异步消费,调试复杂度会急剧上升,因为事件顺序和时序会变得不可控。Harness 里的事件总线更多是进程内发布订阅,作用是解耦和审计。跨进程通信交给 State Store 和调度系统,不要让事件总线承担“模式匹配再触发逻辑”的重任。否则你会陷入循环事件、消息延迟、难以复现的 bug 泥潭。
还有个很实用的判断标准:当一个组件需要超过两周时间去实现时,先质疑它是不是伪需求。很多需求其实靠 prompt 调优就能解决,根本不需要拆出一个新组件。真正值得投入的往往是那些能让任务“可恢复”“可观测”的基础设施,而不是花哨的编排 DSL。
做个原型时,我建议的最小集合是:Task Parser + Agent Runtime + Tool Invoker + Context Manager。这四个组件能覆盖大多数 POC;等 POC 通过,再依次补上 State Store、Observability、Guardrails,最后按业务需要补长期记忆和事件总线。这样既不浪费前期开发时间,也能在关键节点接住复杂性。
Agent Harness 的 12 个组件不是一套需要全部照抄的模板,而是一张检查清单。把这张清单放在手边,每次设计 Agent 时过一遍,问自己:我的 Runner 遇到异常能不能恢复?我的调用路径能不能被追踪?我的 Agent 会不会执行超出边界的工具?这些问题想清楚,你的 Agent 工程化水平会和只调 prompt 的版本拉开明显距离。