Agent 的解剖 -- Harness 到底在解什么

本文基于 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.pyPostgresStorage -- 会话、消息落库,下次打开还能读回来。

状态的核心难点不是"存",而是"哪些该塞进上下文、哪些该留库 "。塞进上下文的决定模型"记得"什么(决定这一轮能否连贯);留库的决定 Agent 跨轮/跨会话还记得什么(决定能否长期延续)。两者是同一件事的两个尺度:上下文管"短期连贯",落库管"长期记忆" 。这里只需记住:没有状态,Agent 是失忆的。

② 行动:Agent 的四肢

行动回答"Agent 怎么碰世界"。它是一套注册 → 执行 → 回流的管道:

text 复制代码
  注册            执行            回流
  ───────────    ───────────    ───────────
  定义工具        Agent 调用它    把结果喂回模型
  暴露给模型      参数校验后跑      成为下一轮推理的输入
  (schema)     (副作用发生)

在 myagent 里,tools/registry.pycalculatorweather 两个普通 Python 函数包装成 AgentScope 的 FunctionToolcalculator.pyweather.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.pyTrustedFunctionTool 覆盖它返回 ALLOW,让自定义工具在 WS 聊天流里免确认执行(这是权衡过的产品决策,详见第四篇《工具系统》)。
  • 人工接管(HITL,Human-in-the-Loop) -- 该请示用户时暂停,而不是自作主张。某些操作(如高危、需确认)Harness 会停下来等用户拍板,再根据决定继续或终止。

控制的重要性在于:前三个内核决定 Agent"能不能做",控制决定它"该不该做、做多久、要不要停"。 没有控制的 Agent,是一个很危险的工具。

七、支撑层:让四内核转起来的底座

四内核是骨架,但骨架自己不会动。让循环真的跑起来、让记忆真的落库、让模型真的被调用,还需要一圈支撑件。支撑件服务于四内核,本身不构成"Agent 的定义",但没有它们四内核全是空转。

text 复制代码
                    支撑层(服务于四内核)
  ┌───────────────────────────────────────────────┐
  │  模型 I/O 桥接   ------ 厂商无关的 LLM_* 抽象        │
  │  异步底座        ------ asyncio、事件循环、流式传输   │
  │  装配            ------ 把 storage/agent/tool 拧起来 │
  │  存储适配        ------ 状态内核的落库抽象           │
  │  可观测性        ------ 追踪、日志、指标             │
  │  配置管理        ------ 环境变量、命名约定           │
  └───────────────────────────────────────────────┘

对照 myagent 代码,每一块都有落点:

  • 模型 I/O 桥接 -- config.pyLLM_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 里的位置,再判断自己要读到多深。

有了这张图,你就不会被某个工具的细节带偏 -- 你知道它只是"②行动"里的一个零件;也不会被某个配置项困惑 -- 你知道它只是"支撑件"在给某个内核供能。骨架在手,深挖不迷路。

相关推荐
神奇霸王龙1 小时前
Agent 准入门控屠夫:5 旗舰 4 维度评估
linux·运维·数据库·ai·ai作画·agent·ai编程
一拳不是超人1 小时前
DeepSeek Harness 为什么敢说"一切皆插件"?拆透 Cordis 引擎的五大核心机制
前端·人工智能·agent
Tisfy2 小时前
Codex:通过编辑配置文件添加带Bearer的自定义MCP
数据库·大模型·agent·codex·mcp
Eloudy3 小时前
ReAct 原理简介
前端·javascript·人工智能·react.js·agent·gpu
x864 小时前
Agent 的搜索引擎:Agentic Resource Discovery 规范,以及它解决不了的信任问题
搜索引擎·agent
苏灿烤鱼5 小时前
公司**不可计算,就自己做操作系统
rust·typescript·agent
程序猿DD5 小时前
OctaFuse Gateway 2.5.0:接入 Responses 端点、更易理解的路由配置
后端·agent
2601_956743686 小时前
工程方案拆解|上海 Agent 开发,技术路线、选型维度与落地能力全景解析
agent·开发经验·上海