开头先亮观点:Agent Harness 和 Agent Runtime 是智能体开发里最容易被人混着说的两个词。你去翻开源项目、看技术博客、跟做 Agent 的工程师聊天,会发现很多场景下这俩词被当成同义替换来用。有人管编排循环叫 Runtime,有人把工具调用框架叫 Harness,还有人干脆统一叫"Agent 框架"。这种混乱不是小事,它直接影响你怎么设计系统、怎么分工、怎么定位线上问题。我最早做智能体项目时也踩过这个坑,花了好几天排查一个"Runtime 层"的问题,最后发现根因在 Harness 层的策略编排上,相当于在发动机里找空调的毛病,方向完全错了。这篇就把两者的边界彻底拆开讲清楚。
先说明白一个事实:这不是纯粹的理论概念辨析。你只要真的动手写过 Agent 系统的核心逻辑,一定会撞上两者边界模糊带来的实际问题——比如回调该挂在哪一层、状态同步该谁负责、工具调用失败该由哪边兜底。这些问题不搞清楚,代码写得再漂亮,跑起来也是提心吊胆。这篇文章适合刚接触智能体开发、准备选型或自研框架的工程师,也适合已经把系统跑起来但总觉得架构哪里不对劲的团队。
先说个我观察到的现象:绝大多数关于 Agent 的教程,讲的是模型怎么调用、工具怎么注册、上下文怎么拼。这些内容确实重要,但它们几乎全部停留在 Runtime 层面。而真正决定一个 Agent"聪明不聪明"的上层策略——比如什么时候该追问、遇到冲突怎么决策、多步任务怎么拆解——属于 Harness 的职责范畴。很多人把这两层混为一谈,导致项目一开始就没分清边界。等到要扩展、要加功能、要排查问题时,代码里到处都是越层的逻辑,改一处崩三处。所以这篇文章的目的很直接:帮你在动手之前,把这两个概念的边界钉死在心里。
1. 两者的本质差异:调度边界与控制边界
先给出一个最直观的区分方式:Runtime 是"执行引擎",Harness 是"操控台"。你可以把 Runtime 想成发动机,它负责把燃料转成动力,把模型调用转成具体的函数执行;而 Harness 是驾驶舱里的仪表盘和方向盘,它决定什么时候踩油门、什么时候转弯、遇到障碍物怎么处理。这个类比虽然朴素,但足够帮你在脑子里建立一个基本框架。
1.1 Runtime 的职责边界:对执行负责
Runtime 管的事情非常纯粹:模型推理、工具调用、上下文管理、内存分配。你可以把它理解成一个"无脑但高效"的执行者。你告诉它"调用 get_weather(tomorrow, beijing)",它就去调用,然后把结果返回。它不关心为什么要调用这个函数,也不关心调用结果之后应该做什么——那是 Harness 的事。
从技术实现看,Runtime 的核心组件包括几个部分:模型适配层(把不同的模型 API 统一成一套接口)、工具执行器(负责把模型的函数调用参数映射成真实代码执行)、会话状态管理(保存对话历史、上下文窗口的滑动和截断策略)、以及阻塞/异步调度的底层机制。这里有个容易被忽略的细节:Runtime 的上下文管理绝不仅仅是"把历史消息拼在一起"。它要做 token 计算、长度控制、关键信息优先级的重新排序,这些策略虽然看起来"有点智能",但本质上是执行层为了让模型能正确处理输入而做的工程优化,不是决策行为,所以仍然属于 Runtime 的范畴。
有一个很典型的现象能帮你判断一个组件是不是 Runtime 的一部分:_它是否在每次 Agent 运行循环中都会被调用到,且不需要"决策"。比如把用户消息转成模型输入格式,这个过程没有任何分支判断,是纯粹的执行逻辑,属于 Runtime。而决定"此时是否需要调用工具、还是直接回复用户"——这是需要决策的,属于 Harness。
1.2 Harness 的掌控范围:对策略负责
Harness 的逻辑层次比 Runtime 高。它负责的核心问题是:Agent 该如何决策、如何规划、如何从错误中恢复、何时终止。
用实际例子来说。一个简单的多工具 Agent 在工作时,会面对这种情况:用户说"帮我查一下明天杭州飞深圳的航班,然后看看那边天气怎么样"。系统先要查询航班列表,然后根据航班落地时间查询天气。这个"先查航班、再查天气"的顺序是谁决定的?不是 Runtime,而是 Harness 中的规划模块在决定。它把用户的大目标拆解成子任务,按依赖关系排序,再逐个交给 Runtime 去执行。
Harness 还负责处理更复杂的决策场景。比如模型返回了一个格式错误的工具调用参数,Harness 需要决定是重试一次、还是换个方式重新提问、还是直接放弃并告诉用户"我无法处理"。这个错误处理策略本质上是控制逻辑,不在执行逻辑的范畴。同理,当可用工具数量很多时,Harness 负责做工具选择——只把当前任务相关的工具暴露给模型,而不是把所有工具的说明一股脑塞进上下文里。
我把两者之间的核心差异用一个表格来呈现,方便对照理解:
| 维度 | Agent Runtime | Agent Harness |
|---|---|---|
| 核心问题 | "怎么执行" | "执行什么、何时执行" |
| 典型模块 | 模型适配、工具执行、上下文管理 | 任务规划、工具选择、错误恢复策略 |
| 决策层级 | 无决策,遵循指令 | 有决策,基于策略 |
| 状态变更 | 会话状态记录 | 任务状态迁移 |
| 失败类型 | 执行失败(API 报错、参数异常) | 策略失败(方向错误、规划不合理) |
| 变更影响 | 影响所有任务的执行效率 | 影响 Agent 的智能水平和行为模式 |
这个表格可以作为团队内部评审架构时的参考清单。如果你在设计系统时,发现某个模块的属性在两个维度之间摇摆不定,说明边界划分有问题,需要重新考虑职责归属。
1.3 划清界限后对系统设计的三个直接影响
边界划清楚的意义不只是概念上爽,它对工程有实打实的好处。我总结了三个最直接的影响:
第一,故障定位效率提高一个量级。当一个 Agent 系统出问题时,你首先能判断问题来自哪一层。模型反复返回错误格式——大概率在 Runtime 的工具执行器或模型适配层;Agent 的行动方向完全不对(比如用户问天气它却去查航班)——这是 Harness 规划策略出了问题。有了这种第一时间的判断,排查范围直接砍掉一半。
第二,团队分工明确。做 Runtime 的人关心延迟、吞吐、稳定性、上下文利用率;做 Harness 的人关心策略效果、任务成功率、决策准确性。这两拨人的 KPI 是不同的,如果混为一谈,会出现"谁都在改、谁都不负责"的局面。我见过一个团队,Agent 的规划逻辑写在 Runtime 的工具调用回调里,结果每次改策略都要重新做回归测试,部署节奏被拖慢了一倍。
第三,框架选型有依据。如果你想用 LangGraph、AutoGen、CrewAI 这类现成框架,你得先分辨它们的哪部分属于 Runtime、哪部分属于 Harness。有些框架擅长编排(Harness 强),但执行层偏弱;有些框架执行效率很高但策略扩展困难。搞清楚两者的边界,你就知道应该在哪个层面做自定义扩展,而不是把框架的每个角落都改一遍。
有一点值得补充:现代 Agent 框架里,Runtime 和 Harness 并不一定由同一个团队开发,甚至可能来自不同的开源项目组合。这种情况下,明确接口约定尤其重要。Runtime 对外暴露什么接口、Harness 如何通过该接口控制执行流,必须形成清晰的契约文档,否则两个独立发展的组件会越走越远,最终无法集成。
2. Agent Harness 的构成拆解:决策层的实际面貌
光说不练是空谈。这一节我直接用工程视角拆解 Harness 应有的组成模块。这里讲的不是某一个具体框架的实现,而是基于多智能体系统和单 Agent 协作场景的通用抽象。
2.1 四个关键模块:规划器、调度器、记忆访问控制、策略注入
规划器(Planner)是 Harness 的"大脑随身版"。它接收用户目标,输出一个可执行的步骤序列。最简单的实现是 ReAct 模式:模型不断交替生成"思考-行动"对,直到任务完成。但工业级系统的规划器没这么简单,它要考虑资源约束(不能无限循环)、步骤间的依赖、以及当前上下文已经获得了哪些信息。规划器通常会被设计成"可插拔"的,因为不同任务的规划策略差异很大——一个客服机器人的规划器和一个代码生成 Agent 的规划器,工作方式完全不同。
调度器(Dispatcher)是 Harness 的"动作派发员"。它根据规划器给出的步骤,决定在什么时机调用 Runtime 的哪个执行函数。调度器还要处理并发问题。比如规划器生成了三个可以并行执行的子任务,调度器要决定是同时并行、还是按优先级顺序执行。并发控制、超时管理、以及部分成功场景的处理,都在调度器的职责范围内。
记忆访问控制(Memory Access Control)是个容易被忽视的模块。Agent 的记忆分成短期工作记忆(当前对话上下文)和长期存储(向量数据库里的历史信息)。Harness 层面的记忆访问控制要做的事情是:判断当前这一步是否需要查询长期记忆、查询哪部分记忆、以及如何把检索结果融入到当前上下文中。这是决策,不是执行——所以它不属于 Runtime。很多团队把记忆模块直接塞进 Runtime,结果导致记忆的读写策略和模型的调用逻辑耦合在一起,每次调整记忆策略都要动底层执行代码,造成不必要的风险。
策略注入(Policy Injection)是 Harness 的"规则引擎"。它负责把业务规则、安全限制、风格约束等注入到决策过程。举个具体例子:一个金融客服 Agent,被要求"当用户询问收益率时,必须同时提示风险"。这个规则不是模型天然知道的,需要通过策略注入在决策层面强制生效。在设计 Harness 时,策略注入模块必须支持动态更新,否则每次改规则都要重新部署整个服务,这在业务快速迭代的场景下完全不可接受。
2.2 Harness 在两种主流框架里的实际呈现
接下来我结合两个业界常用的开源框架,看看 Harness 在这些项目里长什么样。
先看 LangGraph。LangGraph 本质上是一个状态机驱动的编排框架,它的核心抽象是 Graph——节点(Node)和边(Edge)。在 LangGraph 里,你可以显式定义"条件边":比如某个节点执行完后,根据结果内容决定跳到哪个节点。这套机制就是典型的 Harness 实现。你可以用 LangGraph 实现非常复杂的决策流:重试循环、人工介入节点、并行分支汇聚等等。它甚至内置了检查点机制,用于在步骤之间持久化状态——这是 Harness 层的状态管理,而非 Runtime 的会话记录。
再看 AutoGen。AutoGen 的核心概念是对话式多智能体协作。它通过 Conversations 和 Agent 之间的消息传递来实现任务协作。这个框架的 Harness 特征体现在"对话模式"上——你可以定义两个 Agent(比如一个 Assistant 一个 Critic)进行多轮对话,直到达成共识。谁来控制对话的结束条件?谁来决定下一个发言的是谁?答案都在 AutoGen 的 Harness 层机制里。
这两个例子想说明的是:当你选型框架时,要主动识别框架中哪些部分是你需要的 Harness 能力,哪些是它的 Runtime 底子。如果框架的 Harness 能力太弱,你可能要自己搭一层编排逻辑;如果 Harness 能力很强但 Runtime 不满足你的性能要求,你就得考虑替换或者绕过它的执行层。搞清楚边界后,这类判断会清晰很多。
2.3 Harness 设计常见误区:把控制逻辑散落在应用各处
我在实际项目中见到最多的一个错误,是无意间把 Harness 的决策逻辑打散到 Runtime 和应用代码的各个角落。
举个例子:有个团队做客户支持 Agent,他们的代码里,用户在对话中点击"转人工"按钮时,应用层直接调用了一个"人工接管"函数,然后这个函数又在某个回调里修改了对话上下文的状态——这个流程看起来没问题,但实际上跨了三层职责:应用层发起了决策、Runtime 回调做了状态变更、而真正的"转人工策略"(比如判断是否应该先让机器人再尝试一轮)没有形成统一入口。后续要修改转人工的触发条件时,团队成员找了半天才定位到分散在三个文件里的逻辑,改一处还得同步另外两处。
正确的做法是:所有决策点都由 Harness 暴露成统一接口。转人工就是一个策略,应该以"Harness 监听某个事件 -> Harness 决策 -> Harness 调用 Runtime 的方法切换会话状态"这样的链路来设计。哪怕初始实现简单一点,也要让控制流的拓扑是清晰的。
3. Agent Runtime 的执行基础设施:别把这些内容算到 Harness 头上
如果说 Harness 是决策层,那 Runtime 就是支撑所有决策落地的执行基础设施。这一节的重点是帮你识别什么必须放在 Runtime,以及 Runtime 的设计会怎样影响上层 Harness 的可控性。
3.1 模型适配、工具执行与上下文管理的内部细节
模型适配(Model Adapter)是 Runtime 最底层的一环。它的职责是抹平不同模型之间的差异——不管是 OpenAI 格式、Claude 格式、还是开源模型的自定义格式,适配层统一成内部接口供上层调用。一个好的模型适配层不仅仅是把请求格式做转换,它还要处理模型服务的重试策略、token 速率限制、模型返回流式与批量的切换。你会遇到一个现实问题:不同的模型对同样工具定义的写法有细微差别(有的支持并行函数调用,有的必须串行),适配层要把这些差异消化掉,让 Harness 的规划逻辑感觉不到底层的模型变化。
工具执行(Tool Executor)是 Runtime 里最容易出问题的部分。模型返回一个"调用函数A,参数是{...}"的指令,工具执行器要解析参数、校验参数、执行函数、捕获错误并返回给模型。这里有大量工程细节:参数不匹配怎么办(模型经常编造参数)、函数执行超时怎么办、函数返回结果太大超出上下文怎么办。成熟的执行器要做 schema 校验、超时熔断、结果截断。这中间,参数解析和校验虽然看起来带有一些"判断"色彩,但它的本质是机械性的格式匹配,不是策略决策。你要确保这一类逻辑留在 Runtime,让 Harness 专注于更高层的"要不要调这个工具"的决策。
上下文管理(Context Manager)负责构建模型每次调用时的输入。它要考虑的事情包括:历史消息如何截断(只保留最近的 N 轮还是按 token 窗口比例截断)、工具调用的中间结果如何嵌入到对话流、系统提示词如何与动态检索的内容叠加。上下文管理对模型响应质量的影响极其显著。同一个模型,上下文拼得好与拼得差,效果差距可能达到 30% 以上。但它的本质仍然是对已有信息的工程化组织——不涉及"策略决定",所以放在 Runtime 层是合适的。如果这里做的不够好,上层 Harness 的规划再怎么聪明,模型也看不到必要的信息,效果一样打折扣。
3.2 Runtime 层常见的性能瓶颈与扩展挑战
Runtime 的性能瓶颈往往集中在上下文管理和模型调用的吞吐上。上下文管理的瓶颈主要在重复计算。每次模型调用都需要把全部历史消息序列化成 token,如果每次都是从原始消息列表重新编码一遍,token 消耗和时间开销都会指数级增长。工业级实现通常会做"累加式 token 计算"——增量编码新消息,复用之前缓存好的前缀 token。这需要非常细致的内存管理,属于 Runtime 里的硬核优化方向。
模型调用的吞吐问题来自外部 API 的速率限制和网络延迟。一个 Agent 任务可能包含 10 次到 20 次模型调用,如果串行执行,用户体验会非常糟糕。Runtime 要做的是尽可能将独立的模型调用并行化,同时对同一 key 的调用做并发配额管理。这层优化与 Harness 的调度息息相关,但实现机制完完全全属于 Runtime。一个设计良好的 Runtime 会给 Harness 暴露足够的并行控制接口,让上层能够表达"这 3 个子任务可以并行执行",而具体的并发实现完全由 Runtime 完成。
3.3 Runtime 状态管理与 Harness 状态管理的分界点
很多人在状态管理上栽跟头。要搞清楚这个,先理解两种状态的本质不同:
Runtime 维护的是会话状态(Session State)——对话历史、上下文 window 的内容、消息 ID、模型调用的中间产物。这些状态是"执行过程的信息留存",它们的生命周期跟随对话会话。
Harness 维护的是任务状态(Task State)——当前目标是什么、已经完成了哪些步骤、下一个计划动作是什么、当前处于哪个决策分支。任务状态的生命周期跟随一个完整的任务执行过程,一个会话里可能包含多个任务。
这两类状态必须分开存储、分开维护。我见过一个团队把所有状态都塞到一个 Redis 对象里,用一个大 JSON 包含会话历史加任务进度,结果每次并发更新都会出现写冲突,而且排查问题时分不清某个字段是"执行痕迹"还是"决策依据"。后来他们把状态拆成两层,Runtime 的会话状态用消息队列做有序写入,Harness 的任务状态用独立的事务性存储,问题迎刃而解。
有一个简单方法来判断一个字段该放哪层:如果这个字段是给模型看的(会作为输入传给模型),放在 Runtime 层;如果这个字段是给决策模块看的(影响下一步动作的选择),放在 Harness 层。这条规则在绝大多数场景下都准确。
4. 边界模糊的常见陷阱与应对方式:真实项目中的拉锯战
理论讲清楚了,接下来进入实战中最痛苦的环节:真实项目里,两者的边界经常被各种需求拉扯到模糊。我总结了几类最常遇到的陷阱。
4.1 陷阱一:把工具调用的决策逻辑写进 Runtime 的回调里
很多框架允许你在工具调用前/后挂载 hook(回调函数)。于是不少人顺手就把决策逻辑写进回调里——比如"如果工具 A 返回的值大于 100,就自动调用工具 B"。这个逻辑看起来方便,但它是在 Runtime 的执行链路里完成的,绕过了 Harness 的统一规划。
这样写的问题马上就会出现:当你希望整体决策策略能根据上下文动态调整时(比如用户是 VIP 时走一条路径、普通用户走另一条路径),你没办法在 Harness 层统一控制,因为决策逻辑分散在 Runtime 回调的各个角落。排查问题时也是一样,你很难回答"为什么系统会自动调用工具 B",因为触发条件藏在一个不显眼的回调文件里。
应对方式:回调只做事件通知,不做决策。工具执行完的回调里,只做数据格式转换、写入事件日志,然后把结果返回给 Harness 的调度循环去统一决策。刚开始你可能觉得多绕了一层很麻烦,但当地决策逻辑变复杂时,这个分层带来的收益会远超多出来的那一点样板代码。
4.2 陷阱二:"让用户确认"这一步该放哪?
涉及人工介入的场景,是边界最模糊的地方。一个 Agent 在执行关键操作前,需要向用户确认"你确定要这样做吗"。这个"让用户确认"的逻辑,放在哪一层?
放在 Runtime?不对,因为 Runtime 并不能决定"什么时候需要用户确认"——这个触发条件是策略,由业务规则驱动的。比如涉及转账的操作必须人工确认,而普通查询不需要。放在 Harness?对,因为"是否需要人工介入"和"如何暂停任务等待用户输入"是典型的决策与编排逻辑。Harness 应该提供暂停/恢复机制:当规划器发现当前步骤需要用户授权时,它将任务状态挂起到"等待确认"状态,等到用户输入事件到达后,再唤醒任务并继续后续步骤。
区分这类问题的通用方法很简单:如果某个行为在同等条件下对所有用户都一致执行,可以考虑放 Runtime;如果它会根据业务场景、用户属性、上下文而变化,必须放 Harness。用户确认是典型的策略型逻辑,放 Harness 就对了。
4.3 陷阱三:框架默认行为把你带偏
还有一个隐蔽的陷阱——开源框架的默认行为会无意中把边界推向你不想看到的位置。比如某些框架允许你在创建 Agent 时传入一个固定的 System Prompt,这个 Prompt 会由 Runtime 在每次调用时自动附加。如果团队把业务策略直接塞进 System Prompt("当用户情绪激烈时必须转人工"),这块策略逻辑就掉进了 Runtime 层的犄角旮旯里——修改策略需要改 Agent 定义、重新部署、甚至要做对旧任务的迁移,麻烦至极。
应对方式:把 System Prompt 里不变的执行指令(工具使用的格式说明、输出 JSON 的 schema)放在 Runtime 的固定部分;把变化的策略内容(业务规则、当前营销活动话术、敏感话题处理方式)作为变量经由 Harness 在每次任务执行时动态注入。这样一来,策略调整就不需要动 Runtime 代码了。
5. 判别一个 Agent 组件归属的实操标准:三问法
讲了这么多,你很可能需要一个简单好用的判别工具,帮你在实际开发中快速判断某个组件到底属于 Harness 还是 Runtime。这里分享一个我用了很久的"三问法"。
5.1 三问法详解
问自己三个问题,每个问题都有一个明确的倾向性答案:
问题一:去掉这个组件,Agent 还能正常跑吗?
- 如果去掉后,Agent 依然可以完成基础的工具调用和模型推理,只是"不够聪明",那这个组件大概率属于 Harness。
- 如果去掉后,Agent 连基本的"调用个函数返回结果"都做不到,那它是 Runtime 的一部分。
问题二:这个组件的失败,会以什么形式暴露?
- 如果失败表现为模型输出了异常内容、工具调用返回明显错误、程序直接报错——这是 Runtime 层的失败。
- 如果失败表现为模型输出了"合理但方向错误"的内容、执行了不合适的步骤、任务最终没达到目标——这是 Harness 层的失败。
问题三:改一行策略,需不需要重新部署整个系统?
- 如果策略调整(比如修改"什么时候该转人工"的触发条件)应该只是改配置、改规则脚本、甚至只是改一段 Prompt 模板——那么这个策略必须由 Harness 管理。如果它被硬编码到 Runtime 的执行逻辑里,你就被迫做全量部署。
- 反过来说,如果修改的是模型调用重试次数、超时阈值这类执行参数,虽然也是配置项,但它们属于 Runtime 的调优维度。
你要注意,这个"三问法"并不是一个绝对严谨的逻辑推导,而是一个帮助你快速建立直觉的启发式方法。用的次数多了,你会慢慢形成一种条件反射:看到一个组件,自动评估它是在影响"Agent 的思考"还是在影响"Agent 的行动"。
5.2 用"三问法"分析一个真实组件:记忆模块
拿"记忆模块"来做一个完整的分析演练。假设你在设计一个带长期记忆的 Agent 系统,记忆模块要从向量数据库中根据用户问题检索相关信息,然后注入上下文。
第一问:去掉记忆模块,Agent 还能正常跑吗?能跑,但每次对话都是"失忆"状态。所以它不影响基础执行能力。
第二问:它的失败形式是什么?如果检索结果为空,模型会正常且自信地回复一个基于无记忆状态的答案——不会报错,但不准确。这是"方向性"的错误,不是执行异常。
第三问:检索策略调整需要重新部署吗?比如原来召回 top-3 条记忆,现在想改成按时间衰减加权召回 top-5 条——这是策略调整,理想情况下应该以配置或动态规则的形式完成,而不是改一坨 C++ 代码重新编译。
三个问题的答案全部指向 Harness。所以记忆访问控制确实应该由 Harness 负责——但注意,这里说的是"访问控制与召回策略",而不是"向量检索的物理实现"。向量数据库的连接、相似度检索的计算,这些底层执行逻辑属于 Runtime 的基建,Harness 只是在决策层面决定"要不要查、查什么、查完怎么用"。又是一个典型的边界问题——同一件事里同时包含了两个层的职责,这才是实际工程里最考验架构能力的地方。
6. 一次完整的 Agent Harness 与 Runtime 协作实例:电商客服系统
概念全部讲完,用一个完整的实例把整个协作过程串起来。假设你在做一个电商客服 Agent,用户向它询问:"我上周买的手机壳到现在还没发货,能帮我查一下吗?"
6.1 一次查询请求在两层之间的完整流转
Harness 层——规划与调度开始:
- Harness 接收用户消息,规划器判断这是"订单查询"意图,拆解任务步骤:先获取用户身份 -> 获取订单信息 -> 判断订单状态 -> 决定回复策略。
- 调度器发现"获取用户身份"需要调用一个用户服务接口,于是向 Runtime 发起工具调用请求。
Runtime 层——执行动作:3. Runtime 收到调用请求,通过工具执行器调用用户服务接口,拿到用户 ID。 4. Runtime 将工具调用和返回结果都记录到上下文状态中,并把结果返回给 Harness。
Harness 层——继续决策:5. Harness 看到用户 ID 已拿到,决定下一步调用订单查询接口。调度器再次向 Runtime 发起请求。 6. 订单接口返回了"该订单已发货,但物流信息未更新",Harness 规划器需要决策:直接告诉用户"已发货晚点再看物流",还是去调一次物流查询接口再回复?规划器根据"减少用户等待、优先提供确切信息"的策略,决定调用物流查询接口。
Runtime 层——再次执行:7. Runtime 执行物流查询,返回"包裹已到达配送站"。 8. Harness 综合所有信息,决定直接回复用户"手机壳已经在配送途中,预计明天送达",同时附带一个物流链接。
整个过程看起来像一个流畅的直线,但实际上每一轮"决策-执行"的循环,都是在 Harness 和 Runtime 之间来回跳转。Harness 没有亲自去访问数据库或调用 API,Runtime 也没有决定下一步要做什么。两层各司其职,才构成了一个完整的 Agent 行为。
6.2 这个案例暴露出来的两个边界风险点
这个案例里有两个值得注意的小细节,它们暴露出了边界设计的风险。
第一个风险在于"工具调用后结果的呈现方式"。物流查询返回的原始数据可能是一个 JSON,包含快递公司编码、轨迹数组、时间戳等等。直接把原始 JSON 塞给用户显然不合适。这是谁的责任?我的建议是:Raw 数据到自然语言的转换,由 Harness 层的"回复生成策略"负责,因为它决定了"哪些信息该呈现给用户、以什么语气呈现"。Runtime 要做的只是把原始结果原封不动地传给 Harness,不做加工。
第二个风险在于"失败后的补偿动作"。假设物流查询接口恰好超时了,Runtime 返回了一个错误。此时 Harness 可以选:重试一次、换一个物流查询渠道、或者不查物流直接回复"已发货"信息。这个"补偿策略"完全在 Harness 的决策范围内。但很多团队会把重试逻辑写在 Runtime 的工具执行器里——"接口超时自动重试 3 次"——这就让 Runtime 做了本该由 Harness 做的事。正确的分工是:Runtime 提供一个可重试的底层 API,但"是否要重试、重试几次"由 Harness 的业务策略决定。这样区分的好处是,你可以在不修改执行层的情况下,灵活调整重试策略,比如用户是 VIP 时多试一次、普通用户直接放弃。
6.3 如何在实际项目中落地这种分层:演进式设计
最后给你一个实操层面的落地建议。如果你面对的是一个从零开始的新项目,或者一个代码已经比较混乱的存量项目,不要试图一步到位实现完美的分层。建议采用演进式设计:
阶段一:代码结构上物理分层。先把项目按"harness"和"runtime"两个目录/模块分开,即使里面的代码还不够"纯",也先把路径打通。这一步成本很低,但收益立竿见影——后续改代码时,你会自然思考"这段逻辑该放哪边"。
阶段二:用接口隔离双方。Harness 只通过明确的接口调用 Runtime,禁止 Harness 直接访问 Runtime 的内部数据结构。同理 Runtime 只能通过事件通知把执行结果告知 Harness,不能直接调用 Harness 的决策函数。这个阶段会让代码量小幅增加,但可控性大幅提升。
阶段三:逐步上收决策点。审视现有代码里散落的 if-else 决策逻辑,把策略相关的部分逐步收拢到 Harness 的统一调度循环里。每收拢一个决策点,就补一个测试用例,保证行为一致性。
阶段四:状态拆分。最后处理状态归属问题。将会话状态和任务状态彻底分开存储,通过事件关联而非共用可变对象。这个阶段要谨慎,建议在系统相对稳定、有足够测试覆盖后再行动。
这套演进路线已经在我参与过的多个项目中验证过,虽然不是最快的路径,但绝对是风险最低的路径。边界拆分这种事,最忌讳一次性大重构——很容易改到一半发现逻辑根本跑不通,最后回退又浪费时间。小步快跑,每一步都要能独立交付价值,这才是工程上稳健的做法。