本文基于 AgentScope 2.0.4 源码,示例项目为 myagent(AgentScope + FastAPI + Postgres + WebSocket)。
一、为什么需要一张 Harness 的全貌图
Harness 是 LLM 与 Agent 之间隔着的那层壳 -- 状态、行动、循环、控制四块骨架,拼出"自主"二字。
如果只看单篇教程,你很容易学会"怎么调一次 LLM""怎么写一个工具"这样的零件 ;但零件拼不成一台完整的 Agent。真正把"预测下一个 token"变成"能自己决定干什么"的 Agent 的,是模型之外那一整层工程。这一篇就是给它画一张自上而下的骨架图:Harness 是什么、必须由哪几块拼成、它们怎么连、你的输入在里面怎么走。
这篇是独立可读 的 -- 你不必事先读过任何前文。若你读过本系列前几篇(Agent 认知基础、ReAct 循环、工具系统),其中的零件会在骨架图里各就各位;若没读过,这一篇也会从零把骨架讲清。本文只搭骨架,不深挖任何单块机制。
二、Harness 是什么:给纯函数装上身体
先看一个最本质的对比。你第一次调用 LLM 时,写的是这样的代码:
python
import openai
response = openai.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": "北京天气如何?"}],
)
print(response.choices[0].message.content)
这看起来是个函数调用。但把它当"函数"看,恰恰漏掉了 Agent 的本质。
LLM API 是纯函数
一次 LLM API 调用,本质是:
text
prompt ──────────────▶ LLM ──────────────▶ text
(你给的输入) (模型) (模型吐的字)
输入固定 → 输出可复现(近似)
无状态:模型不记得你上次说了什么
无行动:模型不会去查天气、不会执行任何东西
一次即止:调用结束就是终点
它就是教科书里的纯函数:给同样的输入,得到(近似)同样的输出,不改变外部世界,也没有记忆。
Agent = API + Harness
如果你只要"问一句话得一句话",纯函数就够了。但你要的是"Agent" -- 它会自己决定要不要查天气、查哪个城市、查完怎么用结果。这个"自主"从哪来?
模型本身不提供自主。模型只会做一件事:根据输入,预测下一个该输出的 token 。真正让"预测 token"变成"自主行动"的,是模型之外那一整层工程 -- 就是 Harness。
text
┌────────────────────────────────────────────┐
│ HARNESS(壳) │
│ │
│ 让纯函数拥有: │
│ · 状态 ------ 记得(记忆 / 上下文 / 会话) │
│ · 行动 ------ 能做(工具 / 执行 / 回流) │
│ · 循环 ------ 会想(推理 ↔ 行动 的循环) │
│ · 控制 ------ 刹车(终止 / 权限 / 护栏) │
└────────────────────────────────────────────┘
↑ 喂给 ↑ 接收
┌───────────┐ ┌────────────┐
│ LLM │ │ 世界 │
│ (大脑) │ │ (工具等) │
└───────────┘ └────────────┘
"Harness"(英文原意"马具 / 挽具")在 Agent 语境里,指的就是把模型这匹"马"套上车的那套挽具 -- 没有挽具,马只是一匹能跑的生灵;套上挽具,它才变成能载货、能拉车、听你指令干活的工具。这个隐喻贯穿全文:模型是动力,Harness 是把它变成"能干活的 Agent"的那套工程。
控制权转移才是分水岭
纯函数 vs Agent 的分水岭,是谁决定调用几次、怎么处理结果、何时结束:
text
纯函数(你编排) Agent(Harness 编排)
───────────────────────── ─────────────────────────
你决定调几次 LLM Agent 自己决定调几次
你解析返回值 Agent 自己理解结果
你决定何时停 Agent 自己判断任务完成
模型是工具 Agent 是主体,模型是它的脑
所以 Harness 不是"怎么调 LLM 的细节封装",而是把控制权从你的代码转移到 Agent 手里 的那整套机制。你写的每一行 reply_stream() 事件消费、每一个工具中间件、每一条会话存储 -- 都是在搭这副挽具。
三、四内核骨架:状态、行动、循环、控制
Harness 必须由哪几块拼成?我把它收敛成四个内核 。这四个不是"可选项",而是充要条件 -- 缺任何一个,你都得不到一个"正常的 Agent":
text
┌──────────────────────────────────────────────┐
│ HARNESS 四内核 │
│ │
│ ① 状态 State ② 行动 Action │
│ 记忆 / 上下文 工具 / 执行 / 回流 │
│ │ │ │
│ └──────┬───────────┘ │
│ ▼ │
│ ③ 循环 Loop(ReAct) │
│ 推理 ↔ 行动 ↔ 回流 ↔ 再推理... │
│ │ │
│ ▼ │
│ ④ 控制 Control(刹车 / 护栏) │
│ 终止 / 权限 / 确定性 / HITL │
└──────────────────────────────────────────────┘
- ① 状态(State) -- Agent 怎么"记得"?对话上下文、会话、持久化存储。没有它,Agent 每轮都是失忆的新生儿。
- ② 行动(Action) -- Agent 怎么"碰世界"?工具注册、参数校验、执行、把结果喂回给模型。没有它,Agent 只会空谈。
- ③ 循环(Loop) -- Agent 怎么"思考"?ReAct:推理→决定行动→行动→看结果→再推理。没有它,①和②是两块不会自己动的零件。
- ④ 控制(Control) -- Agent 怎么"不失控"?终止条件、权限闸门、确定性、人工接管。没有它,Agent 要么无限循环,要么乱调工具。
一个判断技巧:去掉任一内核,还剩什么?
- 去掉①,剩下的是"每次都从零推理、记不得前文的问答机"------退化成 Chatbot。
- 去掉②,剩下的是"只会讲道理、碰不到世界的空想家"------退化成 Chatbot。
- 去掉③,剩下的是"一次调用就结束、不会自我迭代的脚本"------退化成普通 API 封装。
- 去掉④,剩下的是"可能无限转圈、可能乱来的失控者"------不算可用产品。
四内核是 Harness 的全部吗?不是 -- 它们还需要一圈支撑件才能转起来(模型桥接、异步底座、装配、存储、可观测、配置),但支撑件服务于四内核,是"让骨架能动的血管肌肉",不是骨架本身。支撑件在第六节讲。
四、状态与行动:记忆和四肢
先把两个"资源型"内核放在一起看,因为它们有个共同点:它们都是"东西",是被循环"使用"的。 记忆和四肢本身不思考,思考的是循环。
① 状态:Agent 的记忆
状态回答"Agent 怎么记得"。它分两层:
text
短时记忆 长时记忆
─────────── ───────────
当前对话上下文 跨会话持久化
(这一轮说了啥) (数据库里的会话/消息)
内存中 Storage 存储层
在 myagent 里,短时记忆就是 routers/ws.py 里那一次聊天流的会话上下文;长时记忆就是 storage/_postgres_storage.py 的 PostgresStorage -- 会话、消息落库,下次打开还能读回来。
状态的核心难点不是"存",而是"哪些该塞进上下文、哪些该留库 "。塞进上下文的决定模型"记得"什么(决定这一轮能否连贯);留库的决定 Agent 跨轮/跨会话还记得什么(决定能否长期延续)。两者是同一件事的两个尺度:上下文管"短期连贯",落库管"长期记忆" 。这里只需记住:没有状态,Agent 是失忆的。
② 行动:Agent 的四肢
行动回答"Agent 怎么碰世界"。它是一套注册 → 执行 → 回流的管道:
text
注册 执行 回流
─────────── ─────────── ───────────
定义工具 Agent 调用它 把结果喂回模型
暴露给模型 参数校验后跑 成为下一轮推理的输入
(schema) (副作用发生)
在 myagent 里,tools/registry.py 把 calculator 和 weather 两个普通 Python 函数包装成 AgentScope 的 FunctionTool(calculator.py、weather.py)。模型在推理时"看到"这些工具的 schema,决定要不要用;一旦决定,Harness 负责真正执行这个函数、把返回值作为 tool_result 塞回对话流。
行动的核心难点是**"执行权"**:真正跑函数的是 Harness,不是模型。模型只"提议"调哪个工具、传什么参数,Harness 负责校验、执行、回流 -- 执行权握在 Harness 手里,这正是工具系统值得单独深挖(见第四篇《工具系统》)的原因。这里只需记住:没有行动,Agent 只会空谈,碰不到真实世界。
五、循环与解码:大脑怎么思考
③循环是四内核里最像"活物"的一个,它把①和②接到模型上,让整台机器自己转起来。这一节要讲两件事:循环本身,和循环里的"解码"。
③ 循环:推理 ↔ 行动
ReAct 循环(Reasoning + Acting)是 Agent 的心跳,它的形状是:
text
┌────────────┐ 推理(Reasoning)
│ 看上下文 │ ────▶ 模型吐文本 或 工具调用意图
└────────────┘ │
▲ │ 若是工具
│ ▼
│ 行动(Acting)执行工具
│ │
│ ▼
└────── 回流结果,再推理 ──┘
直到:模型只吐文本(无工具调用)→ 这一轮结束
这个循环在 AgentScope 里就是 _reply_impl() 的主干(第三篇有源码级深挖),myagent 在 routers/ws.py 里通过 message_bus.subscribe(...) 订阅这个循环吐出来的一连串事件,再推给前端。循环决定了 Agent 是"一次调用"还是"自主迭代"。
解码:循环里的翻译官
循环跑起来后,模型吐出的不是"结构化的指令",而是一串 token。Harness 必须把这串 token"翻译"成两件事之一:
text
模型输出(一串 token)
│
▼
┌─────────────┬──────────────────┐
│ 普通文本 │ 工具调用意图 │
│ → 给用户 │ → 交给行动内核 │
└─────────────┴──────────────────┘
这一步叫解码 (这里"解码"指把模型的输出翻成 Harness 能理解的结构化指令)。模型说"我想调 calculator"是一回事,Harness 能不能从输出里准确解析出 "工具名是 calculator、参数是 1+2*3"是另一回事。解码错了,后面的执行就全错了 -- 这是 Agent 工程里非常容易踩坑的一环,也是"模型会说话"和"Harness 能听懂"必须分开解决的原因。
先记住骨架:循环负责"转",解码负责"让循环读得懂模型的吐字"。两者配合,Agent 才可能推理→行动→再推理。
六、控制:刹车与护栏
④控制是四内核里最容易被新手忽略、但产品化时最关键的一个。它回答"Agent 怎么不失控"。失控有三种典型形态:
text
失控形态 Harness 的护栏
──────────── ────────────────
无限循环 终止条件(max_iters)
乱调工具 权限闸门(check_permissions)
说不清就乱来 确定性 / 人工接管(HITL)
- 终止条件 -- ReAct 循环不能无限转。AgentScope 的
ReActConfig(max_iters=5)就是刹车:myagent 的翻译子 agent(agents/translator.py)就设了max_iters=5,防止翻译这种简单任务陷入无谓的反复推理。 - 权限闸门 -- 工具不能谁都能调。AgentScope 的工具有
check_permissions()权限检查;myagent 里tools/registry.py的TrustedFunctionTool覆盖它返回ALLOW,让自定义工具在 WS 聊天流里免确认执行(这是权衡过的产品决策,详见第四篇《工具系统》)。 - 人工接管(HITL,Human-in-the-Loop) -- 该请示用户时暂停,而不是自作主张。某些操作(如高危、需确认)Harness 会停下来等用户拍板,再根据决定继续或终止。
控制的重要性在于:前三个内核决定 Agent"能不能做",控制决定它"该不该做、做多久、要不要停"。 没有控制的 Agent,是一个很危险的工具。
七、支撑层:让四内核转起来的底座
四内核是骨架,但骨架自己不会动。让循环真的跑起来、让记忆真的落库、让模型真的被调用,还需要一圈支撑件。支撑件服务于四内核,本身不构成"Agent 的定义",但没有它们四内核全是空转。
text
支撑层(服务于四内核)
┌───────────────────────────────────────────────┐
│ 模型 I/O 桥接 ------ 厂商无关的 LLM_* 抽象 │
│ 异步底座 ------ asyncio、事件循环、流式传输 │
│ 装配 ------ 把 storage/agent/tool 拧起来 │
│ 存储适配 ------ 状态内核的落库抽象 │
│ 可观测性 ------ 追踪、日志、指标 │
│ 配置管理 ------ 环境变量、命名约定 │
└───────────────────────────────────────────────┘
对照 myagent 代码,每一块都有落点:
- 模型 I/O 桥接 --
config.py用LLM_BASE_URL / LLM_API_KEY / LLM_DEFAULT_MODEL这套基础设施中性的命名,让应用只认 OpenAI 兼容端点,不绑定任何具体网关。这是"模型桥接"的抽象精髓:换网关不改代码。 - 异步底座 -- asyncio 是 Agent 能"边推理边吐事件"的根基(第二篇)。
reply_stream()就是异步生成器,ws.py靠它逐帧推流。 - 装配 --
server.py是 App 工厂:create_app(storage=..., message_bus=..., extra_agent_tools=..., custom_subagent_templates=...),把存储、消息总线、工具、子 agent 模板全部拧进一台应用;deps.py负责创建storage等实例。 - 存储适配 --
storage/_postgres_storage.py把会话/消息落库,是状态内核的地基。 - 可观测性 --
observability.py提供结构化日志、request_id_var请求追踪、ObservabilityMiddleware工具调用观测;middleware/request_trace.py是 HTTP 请求追踪。没有它,Agent 一跑起来就是黑盒,出问题无从查起。 - 配置管理 --
config.py集中读环境变量并校验,遵循LLM_*前缀约定,让应用与部署细节解耦。
支撑件和四内核的关系,好比发动机(循环)与油箱/管路/仪表(支撑件):发动机是灵魂,但没油管它转不起来,没仪表你不知道它烧得怎么样。
八、一次输入的生命周期:把整台机器跑一遍
前面是"拆",这一节"装回去" -- 把四内核和支撑件串成一次用户输入从进来到返回的完整流水线。以 myagent 的 WS 聊天为例:
text
用户: "北京天气如何?"
│
▼
① 状态 收到消息,装入当前会话上下文(短时记忆)
│
▼
③ 循环 把上下文交给模型 → 模型说"想调 weather 工具"
│
▼
解码 从模型输出解析出: 工具=weather, 参数={city:"北京"}
│
▼
② 行动 Harness 执行 weather 工具 → 拿到天气数据
│
▼
④ 控制 权限检查(TrustedFunctionTool ALLOW)→ 通过
│
▼
回流 tool_result 塞回上下文,喂给模型再推理
│
▼
③ 循环 模型这轮只吐文本"北京今天晴,18-26°C"(无工具调用)
│
▼
文本 → 通过 WS 推回给用户(reply_end)
对照 ws.py 里那条协议注释,每个事件都有落点:
text
{type: "reply_start"} ← 循环启动
{type: "tool_call_start"} ← ②行动:开始执行工具
{type: "tool_result_end"} ← 回流完成
{type: "text_delta"} ← ③循环吐文本,逐帧推
{type: "reply_end"} ← 循环结束
这一条流水线,就是整台 Agent 的"一次心跳"。你会发现:四内核不是四个独立模块,而是同一条流水线上的四个环节 -- 状态供料、循环驱动、行动做工、控制把舵,支撑件全程保障。
九、四内核之外:这张图怎么用
到这里,Harness 的解剖就完成了。你手里现在有一张图:
text
模型是大脑(动力源)
四内核是身体(骨架):
① 状态 记得 ② 行动 能做 ③ 循环 会想 ④ 控制 刹车
支撑件是血管和仪表(让骨架能动、可知)
这张图是"自顶向下"的骨架,而单篇教程往往是"自底向上"的零件讲解。两者互为补充:零件讲"这是什么、怎么用",骨架讲"它长在哪儿、为什么需要"。往后你读任何一篇关于某块机制的深挖,都能先在这张图上定位它在整台 Agent 里的位置,再判断自己要读到多深。
有了这张图,你就不会被某个工具的细节带偏 -- 你知道它只是"②行动"里的一个零件;也不会被某个配置项困惑 -- 你知道它只是"支撑件"在给某个内核供能。骨架在手,深挖不迷路。