☰
Agent Harness Runtime:从无状态调用到可恢复运行系统的架构演进
2026/10/20 23:06:15 网站建设 项目流程

1. Agent 真正的分水岭:不是模型效果,而是运行时怎么组织

先聊一个我反复看到的现象。很多团队的第一版 Agent 都长一个样:写一个while True,把用户问题塞给大模型,拿到结果再塞回去,循环几次出结果就收工。Demo 阶段一切正常,等接上真实业务、多用户并发、工具权限、中断恢复之后,问题一个接一个冒出来——会话状态去哪了、工具调用乱套了、模型在同一个坑里反复打转、进程重启用户就得重新聊。这时候大家才意识到,AI Agent 架构的核心难点根本不在模型本身的推理能力,而是怎么把一次无状态的 LLM 调用,组织成一个有状态、可观测、可恢复的运行系统。

这个"运行系统",就是我标题里说的 Harness Runtime。Harness 这个概念可以理解成给 Agent 套一个"运行容器":它负责调度模型调用、管理上下文窗口、接收工具返回值、保存每一步轨迹、在崩溃后恢复现场。这也是本文真正想解决的选型问题——你做的 Agent 到底需要武装到哪一层,什么时候只需要裸调模型,什么时候必须引入完整的 Harness Runtime。

这篇文章适合两类人看。一类是刚起步做 Agent 应用,被各种架构图看得云里雾里,想知道第一步该从哪下手的开发者;另一类是已经在跑 MVP,正被会话丢失、循环失控、上下文爆炸折磨,准备重构但下不了决心的人。我会从一次 LLM 调用的最小闭环讲起,逐步拆到完整 Runtime 的落地骨架,把每一层的设计理由和实测教训都交代清楚。

2. 选型之前先想清楚四件事:别让框架替你拍板

我见过不少项目翻车的起点,不是技术选型选错了,而是根本不知道自己在为哪个问题做选型。现在市面上的 Agent 框架、编排引擎、运行时方案一大堆,库和库之间的设计哲学差异极大,有的追求流程可视化,有的追求纯代码自由,有的强调事件驱动。如果你脑子里没有一杆秤,就只能跟着框架走,最后被框架的设计假设绑架。

我建议在打开任何框架文档之前,先回答四个问题。

第一个问题:你的 Agent 需要跨多久的生命周期?一次请求内完成回答,还是一个业务过程可能要持续几十分钟甚至几天?前者(比如"帮我总结这份文档")基本不需要状态持久化,模型调用完事就结束;后者(比如"帮我处理一批供应商合同,中途缺材料要等用户补传")就必须有状态存储,因为执行随时可能被挂起,再被用户动作唤醒。生命周期一长,光靠进程内内存就撑不住了,你需要把状态落到外部存储。

第二个问题:Agent 会不会调用外部世界?工具调用的引入是一个巨大的架构拐点。没有工具的 Agent 是"文本进、文本出",你只需要管理模型输入输出;有工具的 Agent 意味着系统里出现了"模型生成的参数→真实执行副作用"这条链路,你不仅要校验参数合法性,还要决定工具返回值怎么塞回上下文,以及执行失败后模型要不要换个策略重试。这一步直接决定了你要不要在调用层之上引入编排层。

第三个问题:你需要多细的可观测性?这个问题经常被忽略。Demo 阶段出问题你直接加日志打印就行,但生产环境的 Agent 你必须在事故发生时知道:模型在第几步做了哪个决策、当时上下文窗口里有什么、工具传入了什么参数。这不是"日志打多点"能解决的,它要求运行时从设计第一天就贯穿 trace 链路。市面上很多轻量方案根本没有埋点设计,事后排查极其被动。

第四个问题:失败之后允许怎么恢复?Agent 执行过程是模型一步步决策叠加出来的,中途任何一步失败(模型超时、工具报错、进程崩溃、网络分区),用户期望的是什么?是从头重来一次,还是从最近的检查点继续?不同答案对应完全不同的架构复杂度。如果允许断点续跑,那你的每一步执行都要能被序列化、被定位、被重新执行。

把这四个问题过一遍,再回头看"要不要上 Harness Runtime"就有答案了。四个答案都是"否",裸调模型就够了;只要有两个以上是"是",你就已经在做运行时系统了,这个认知越早建立越好。技术选型本质上不是选框架,是选你愿意为多少确定性支付多少复杂度。

3. 调用层的最小自洽单元:封好一次 LLM 请求是地基工程

很多人觉得 Agent 架构很玄,其实最底层的单元就是一个大模型 API 调用。但注意,即使是这一层,也藏着不少坑。我见过太多项目把模型调用代码直接裸写在业务逻辑里,Prompt 拼接、API 参数、超时重试散落各处,等后面要加上下文管理、追踪链路时,才发现根本没有一个统一的入口。

3.1 一次调用的四大组成:Prompt 模版、参数约束、输出校验、容错策略

一个能进生产的调用封装,至少得做四件事。第一,Prompt 模板和业务变量分离,不在源码里硬拼字符串;第二,把模型支持的结构化输出能力(函数调用/JSON 模式)显式声明出来,让模型"被迫"按规矩回答;第三,拿到原始响应后立刻做格式校验和容错解析,模型经常返回合法但不符合预期的结构;第四,封装超时和重试,LLM 服务抖动是常态不是异常。

拿工具调用来说,模型厂商提供的 function calling 可以让返回结果变成结构化的参数列表,这是 Agent 和工具链衔接的桥梁。但实际操作中,模型经常犯三类错误:参数名对不上、参数类型传错、应该调工具的时候不调。这就意味着你不能把模型的原始返回直接当最终结果用,必须在调用层就做一轮"行驶前检查"——校验参数 schema、检查必填字段、对缺失项做兜底处理。我通常的处理方式是把校验结果也作为一个结构化事件回传,如果校验不通过,把错误描述拼进下一次调用的上下文,让模型自己修正。

3.2 为什么每次调用都必须附带完整调用链信息

从架构角度看,调用层还承担一个隐性职责:它是所有可观测信息的采集点。每次调用发生时,你应当记录模型请求参数、耗时、Token 消耗、返回内容、校验结果。这些数据在开发期看着多余,上线后用 g 攒下来的 trace 才能帮你画出一张实际的 Agent 决策轨迹图。我自己有个习惯:调用封装里强制生成一个 request_id,并且让这个 id 贯穿后续所有环节。这成本很低,但排障价值极高——没有这个 id,你想把模型某一步的输入输出串起来,只能靠时间戳瞎猜。

这一层要做好,本质上就是一句话:把大模型当成一个不稳定的第三方 HTTP 服务来对待,而不是当成一个智能引擎。用工程手段抵消它的不确定性——强校验、熔断、重试、追踪。地基打稳了,上层才有资格谈架构。

4. 上下文管理:Agent 的记忆问题不是提示词技巧,而是系统设计

Agent 跑得越久,上下文管理越会成为头号工程难题。大模型的上下文窗口看起来很大,但被对话历史、工具调用记录、业务文档一塞,很快就不够用了。更麻烦的是,模型对上下文中各部分的注意力并不均匀,无关信息多了甚至会干扰决策。所以"记忆"不是一个把聊天记录存下来的问题,而是一个预算管理问题。

4.1 上下文预算分配:谁的优先级高,谁占的 Token 多

我在实践中会把上下文分成几档,按优先级分配预算。系统提示词是最高优先级,业务背景和用户核心目标也是,这部分的 Token 不能省;工具列表视数量和必备程度控制长度,能用名字加一句话描述就绝不放完整说明书;历史对话是最先被压缩的部分,超过预算就丢给总结机制。你会发现,常见的"记忆压缩"其实就是在用不同的粒度管理上下文预算——保留关键事实摘要,丢弃寒暄和过程性细节。

一个常见误区是试图把工具说明书全量塞进上下文。有些 Agent 接入三四十个工具,每个工具又带着详细的参数描述,光工具定义就消费了一两万 Token。模型在过长的工具列表里反而会"挑花眼",命中率显著下降。我比较认可的做法是工具分组 + 动态加载:先根据用户意图粗筛哪类工具可能被用上,再把候选工具定义注入上下文。这一步放在调用层外面做,叫意图路由,效果比全量注入好很多。

4.2 压缩、截断、遗忘:三种记忆策略的取舍

上下文超预算时,有三种常见策略:截断、摘要、检索。截断最简单,直接丢最老的消息,但会丢关键信息;摘要是用一个额外的模型调用把历史对话总结成要点,保留高信息密度,但每次摘要是开销,而且多轮摘要会累积失真;检索是针对大知识库场景的,把历史记录向量化,按需取回相关片段,复杂度最高。

三者在真实系统里是组合使用的,不是三选一。我的典型做法是:对话超过一定轮数就触发摘要,摘要结果作为新历史段的开头注入;最老且连续几轮未被检索命中的内容会被归档到向量库,仅保留指针;最新几轮完整保留,因为模型做决策更依赖"最近的言行"。这个组合设计执行下来效果最稳,既控制了 Token 消耗,又保留了关键信息。

还有一个工程上的细节值得提:上下文窗口里塞进了大量历史之后,模型的延迟会肉眼可见地变慢。有些模型对长上下文的处理是二次传播,算力消耗随长度非线性增长。所以你压缩历史不只是省钱,更重要的是保住响应速度。我实测下来,把上下文控制在窗口上限的 60% 以内,延迟表现最理想。这个比例不一定普适,但你可以拿自己的业务先测一轮,大概率会得到类似结论。

5. 工具层与编排层的职责边界:让模型做决策,但永远让代码做执行

引入工具调用之后,Agent 系统就会分裂成两条线:一条线是模型的思考线,负责决定下一步做什么;另一条是系统的执行线,负责实际调用工具、处理副作用。这两条线的边界一旦模糊,项目后期维护就是灾难。

5.1 工具注册表:描述、入参校验、权限控制是三位一体的

我强烈建议把工具层设计成一个显式的注册表,而不是散落各处的函数。注册表里每条工具记录包含:工具名称、一句话描述(给模型看的)、参数 JSON Schema(校验用的)、执行函数、权限级别、追踪钩子。这样一个工具要接入 Agent,只需要在注册表里加一条记录,上层编排逻辑完全不用动。

经常被忽略的是工具入参校验。模型返回的参数是以字符串或嵌套对象的形式存在的,实际执行前你必须对照 Schema 校验一次:类型对不对、枚举值是否合法、必填字段有没有缺失。不要迷信模型的输出质量,它可能在一次对话中正确调用十次工具,第十一次传回一个空对象。我遇到过一个印象深刻的案例:模型调用地图工具时把经度和纬度两个参数顺序写反了,导致 Agent 拿着一个错误位置继续推理,客户差点投诉数据不准。加一层入参校验就能拦住这一类错误。

权限控制也要放在工具层。不同用户、不同场景能调用的工具集合应该不同,比如"只读查询"和"下单付款"绝不能混在一个权限里。具体做法是给每个工具标注权限级别,Runtime 在执行前做一次鉴权。漏掉这一步的后果很直接:Agent 被莫须有的 Prompt 注入骗着执行了高权限操作,这是当前真实发生的大量安全事故的根源。

5.2 编排循环:把"模型想做什么"翻译成"系统做什么"的翻译官

然后是编排层。编排层的工作核心是维护一个循环:拿到模型决策→执行→把结果反馈给模型→让模型再做决策。市面上主流的设计有两种,一种是 ReAct 风格(思考+行动交替),模型在每一步输出"下一步动作";另一种是状态机风格,把每个业务阶段定义为明确的状态,Agent 只能在状态内动作,状态间跳转由显式规则控制。

ReAct 风格自由灵活,但循环次数不可控,模型容易在原处打转;状态机风格刻板,但每一步都知道自己在哪,边界清晰。真实系统里我倾向于混合模式:用状态机约束大的业务环节(比如"收集材料→审核→批复"),在每个环节内部用 ReAct 式的自由决策组织细节动作。这样既有流程上的确定性,又有执行上的灵活性。

编排层还必须设置循环终止条件:最大步数、达成目标的判定函数、用户中断、确认等待。没有终止条件的 Agent 就是一个失控的循环,账单会跑到你还不知道。我把终止条件分成三类来设计:"任务完成"型(目标条件满足)、"资源耗尽"型(达到步数或 Token 上限,主动收敛并返回现状)、"用户介入"型(给用户选择是继续还是终止)。这三类条件同时存在,任何一个先触发就先退出循环。

6. Harness Runtime 的落地骨架:让 Agent 从"跑完即焚"变成"可恢复的系统"

前面几层覆盖了 Agent 正常工作链路,但离一个能上生产、能长期运行的系统还差一层:运行时生命周期管理。这一层就是标题里的 Harness Runtime,解决的问题是状态怎么存、执行怎么续、故障怎么恢复、并发怎么控制。很多团队把 Agent 代码写出来了,却没法上线,就是差在这一层。

6.1 状态快照与断点续跑:序列化的是"执行现场",不只是聊天记录

一个带状态恢复能力的 Runtime 至少要能保存两类东西:一是会话级状态(用户目标、业务参数、已完成哪些步骤),二是步骤级状态(当前在哪一步、这一步模型的输入输出是什么)。一个朴素而实用的模型是:每次 Agent 执行完一个步骤(一次模型调用加一次工具执行),就把整个执行现场序列化到外部存储(数据库或对象存储),记下当前节点指针。崩溃后重启,从最近快照恢复现场,继续执行。

序列化快照的粒度很讲究。粒度太粗(比如整个任务结束时才保存一次),中断就丢失很多进度;粒度太细(每次模型 Token 都存),存储和 IO 开销大得没法看。我的经验是按"一个有业务边界的动作"为粒度,比如"调完一次工具并拿到结果后"。这样中断损失最多恢复到一个工具调用之前,用户感知基本无差别。

序列化的内容里有一个容易被忽略的关键部分:工具执行结果。大模型 API 返回的文本结果丢掉可以重新生成,但工具副作用(比如已经发送了通知、已经创建了订单)是不可重播的。所以快照必须记录"这个副作用已经发生",否则恢复后 Agent 会傻乎乎地重复执行一次下单。我用一个 action_log 表记录每一步的副作用事件,恢复时先检查有没有已执行的动作,再决定要不要重新触发。这一条是断点续跑能从 Demo 走到生产的关键,非常建议在一开始就设计进去。

6.2 一个极简 Runtime 的骨架设计:事件循环加三张表

说了这么多理论,直接看一个最小可实现骨架。技术栈上我不预设语言,但下面的逻辑结构是所有 Runtime 共通的。容器里有一个持久化层(我用的是关系库加 JSON 列),负责存会话快照和事件日志;一个调度器,负责把模型调用、工具执行、人工确认这些动作串起来。

# 核心循环:每次从存储里拉回某个会话的快照,执行一个步骤,写回 def run_agent_iteration(snapshot): # 1. 从快照还原当前状态 state = restore_state(snapshot) # 2. 组装当前上下文 messages = build_messages(state) # 系统提示 + 历史 + 工具结果 # 3. 请求模型决策 decision = llm_call(messages, tools=state.active_tools) # 4. 校验模型输出,如果格式非法则把错误回填上下文再调一次 if not validate_decision(decision): messages.append(error_feedback(decision)) decision = llm_call(messages, tools=state.active_tools) # 5. 执行工具(如果有),或者产出最终文本 if decision.tool_call: result = execute_tool(decision.tool_call) event = store_side_effect(snapshot.session_id, decision.tool_call) # ^ 副作用先记账再落库,防止恢复时重复执行 state.append(("tool_result", result)) else: state.append(("final", decision.text)) mark_completed(snapshot.session_id) # 6. 保存快照,返回是否结束 save_snapshot(snapshot.session_id, state) return state.should_stop

配合的存储层就三张表:sessions存会话级元信息,snapshots存每一步的完整状态 JSON,events存每一步的执行轨迹和副作用记录。这三张表设计好了,断点续跑、并发恢复、排查问题就都有抓手了。

-- 会话表 CREATE TABLE sessions ( id TEXT PRIMARY KEY, user_id TEXT, status TEXT, -- active / completed / failed created_at TIMESTAMP ); -- 状态快照表 CREATE TABLE snapshots ( id TEXT PRIMARY KEY, session_id TEXT, version INT, -- 递增版本号 context JSONB, -- 完整上下文状态 node_pointer TEXT, -- 当前执行节点 updated_at TIMESTAMP ); -- 副作用事件表 CREATE TABLE events ( id TEXT PRIMARY KEY, session_id TEXT, step_id TEXT, event_type TEXT, -- llm_call / tool_call / user_message / final payload JSONB, -- 具体内容 created_at TIMESTAMP );

这个骨架虽然简陋,但已经具备完整的 Runtime 能力。你可以在它上面叠加网络接口、多租户、权限系统,核心循环不需要改。

6.3 可观测性不是附加功能,是 Runtime 的默认配置

很多团队把可观测性当上线前才做的事,这是顺序上的大错误。Agent 的排障比传统应用难得多,因为它的"逻辑链路"是模型动态生成的,没法预先枚举。唯一可靠的排障方式就是看轨迹——某一次任务从开始到结束,模型每一步看到了什么、说了什么、调了什么工具,必须完整还原。

我在 Runtime 里把 trace 设计成了必选链路,所有事件统一进入上文提到的events表,再按会话维度聚合。这样有三个好处:出事故可以逐帧回放;迭代 Prompt 时能用 trace 对比哪一版表现稳定;需要构建评测数据集时,这些 trace 直接就是真值样本。说实话,一个没有完整 trace 的 Agent 系统,我根本不敢把它投到生产环境,因为你连它为什么做错都不可能在事后搞清楚。

7. 实测踩坑记录和一条现实可行的选型路线建议

最后聊一些我在实际项目中踩过的坑,以及根据这些教训总结出来的架构演进路线。

第一个坑是"循环失控"。早期版本我设了最大循环步数,结果模型在一个"结果符合预期但用户没明确点头"的边界条件下反复补问,把步数烧完了还在原地打转。后来加入了一个"意图置信度"机制:系统内置关键目标的达成判定函数,一旦函数判定目标达成,就强制收敛并输出总结,不再等模型自己决定是否停止。这比单纯限制步数有用得多。

第二个坑是"工具返回格式不统一"。有的工具返回纯文本、有的返回 JSON、有的返回二进制,塞回上下文时格式完全混乱,模型经常误解。后来我在工具层强制统一成"两类结果":机器可读的结构化对象(供上下文注入)和人类可读的展示文本。模型决策依赖的是结构化对象,展示文本只用于给用户看。这个分离让模型的理解准确率明显上了一个台阶。

第三个坑是"状态恢复后重复执行"。在一次压测中,服务进程在发送通知邮件后崩溃,恢复时 Agent 重新执行了发送邮件的动作,用户收到了两封内容一样的邮件。这就是前面提到的副作用记账没做好的后果。后来我把"可产生副作用的工具"和"查询类工具"分开登记,前者强制走副作用事件表,先记账再执行,这个问题才算根治。

第四个坑是"上下文越滚越大导致响应变慢"。我试过在上下文超限时简单截断早期对话,结果模型因为丢失了用户最初的核心诉求,开始做出偏题的决策。痛定思痛后升级成"分层摘要加关键事实抽取",每次要压缩前先让模型抽取出不可丢弃的用户目标和约束条款,这些内容用固定位置的"backbone memory"注入,才稳定下来。

下面是基于这些经验的选型路线建议,按项目阶段划分:

  • 阶段一:单次调用精确回答。项目刚起步,需求固定,不接工具或只接一两个查询型工具。这个阶段裸调模型加上第一节说的调用层封装就够了。不需要框架、不需要运行时引擎,把调用层写扎实、留好 trace 埋点,是性价比最高的投入。
  • 阶段二:循环决策加多工具协作。需求开始需要多步推理、穿插调用多类工具。这个阶段你需要一种编排方式(ReAct 或状态机)和一份工具注册表。可以用市面上的编排框架,也可以自己写前面那个几十行的循环骨架,看团队是否有精力和偏好去控制底层。
  • 阶段三:完整 Harness Runtime。多用户并发、长生命周期任务、需要断点恢复和分析排障,这几条一旦出现,你就需要把状态持久化、副作用记录、事件溯源做完善,也就是升级到完整的 Runtime。这个阶段不是"选一个框架"能解决的,而是一个系统的架构设计问题,必须专门投入人力。

按照这个路线走,你永远不会做过度架构:阶段一的需求不上框架,阶段二的需求不套 Kubernetes,阶段三才会引入完整运行时。反过来,如果一个项目已经从阶段一跑到了阶段三,每一次迁移要做的就是对照核心循环把缺失的块补齐——这比推倒重来和全盘照搬要稳得多。

我个人的体会是,Agent 架构选型没有银弹,但有一条非常确定的原则:先搞清楚你的系统要管理什么样的生命周期,再决定武装到哪一层。从一次 LLM 调用到完整 Harness Runtime,中间每一层的引入都应该是对具体痛点的应答,而不是对行业热点的追逐。扎实做好调用层、仔细设计工具边界、时刻记得副作用记录,比用再酷炫的框架都管用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询