做了几年 Agent 相关系统,我有一个越来越强烈的感受:现在缺的不是能跑通 Demo 的代码,而是一套真正经得起生产环境考验的 Agent Runtime。很多人把“编排”想得很玄,觉得只要模型够聪明,循环调用工具就行。但真实工程里,模型返回不稳定、工具调用有副作用、进程随时可能崩、Token 成本还会失控,这些问题不靠 Runtime 层面的设计去兜住,业务代码迟早变成一团乱麻。这篇内容我想完整梳理一下 Agent 编排的全景:底层抽象怎么划、运行循环怎么设计、状态和记忆怎么管、工程落地要补哪些基础设施,以及我踩过的那些坑。核心目标是帮大家理解一件事——什么是“好用”的 Agent Runtime,以及怎么从零把它搭起来。适合正在做 Agent 应用、想把零散代码沉淀成平台能力的工程师阅读,也适合准备做 Agent 基建的团队做参考。1. 先搞清楚 Agent Runtime 到底在解决什么问题要谈 Runtime,先得承认一个现实:Agent 应用和传统应用有本质差异。传统程序的执行路径是确定的,if-else 分支写得再复杂,每一步都是预设好的;Agent 的路径是模型实时生成的,同样的输入,这次可能先调 A 工具再调 B,下次可能直接给出答案。你没法预先画出每一条调用链,只能提供足够的约束、观测手段和容错能力,让模型在可控范围内自由发挥。“编排(Orchestration)”这个词,指的就是对这条动态路径的控制与承载。它在解决的核心问题是:模型循环 + 工具调用 + 状态管理 + 基础设施,这一整套公共能力要沉淀成独立层,让业务开发只关心 Agent 的目标、工具和策略,而不必重复实现循环、并发、重试这些脏活。我见过不少失败的项目,模型能力其实没问题,但工程侧一塌糊涂。出现过的最典型现象,就是业务代码退化成一大锅“胶水代码”。明明只是调几个工具,代码里却挤满了模型调用、JSON 解析、异常捕获、上下文拼接、日志输出。一旦任务链路变长,变量满天飞,中间结果靠局部变量传来传去,想加一个新工具都要担心会不会影响全局。Pexels 有一位同事的项目就是这样,把一个多轮调用的 Agent 写到 2000 多行,最后所有逻辑纠缠在一起,改一个参数会牵连到好几个分支,只能重写。这个场景不是个例,而是很多人迟早要撞上的墙。独立出 Runtime 层,本质是把复杂度降下来:让该稳定的地方稳定,该灵活的地方灵活。1.1 Runtime 的边界:该管什么、不该管什么我倾向于把 Runtime 的职责收窄为四件事:调度、状态、资源、观测。调度:负责模型循环的推进,包括要不要继续、下一步调哪个工具、执行完工具后如何把结果反馈给模型。状态:负责维护会话上下文、变量、临时结果,以及和持久化存储之间的同步。资源:负责并发控制、限流、超时、Token 预算、工具执行配额。观测:负责记录运行轨迹、耗时指标、Token 消耗和成本分摊。在这四件事之外,比如 Agent 的目标怎么设计、工具怎么实现、提示词怎么写,这些不应该放进 Runtime。好的 Runtime 要尽量保持通用性,把业务差异交给上层配置和工具层。我见过一些团队把业务规则直接写死在编排引擎里,光看前期是快,但换一个场景就要改引擎,最后谁也不敢动核心代码,这比业务胶水更可怕。1.2 评估 Runtime 好不好的五个维度我用来判断一套 Runtime 是否“好用”的标准只有五条:接入成本:新写一个 Agent 需要多少样板代码。如果声明好工具、写好目标,几十行就能跑起来,说明抽象合理。可观测性:是否天然产出结构化日志和 trace,不需要业务代码额外埋点。容错能力:模型输出不规范、工具超时、网络抖动,这些是否都有默认策略。可扩展性:能否在不修改 Runtime 的情况下,加入新的工具类型、记忆策略、编排模型。运行成本可控:能否对每次任务做 Token 和耗时预算,超了就中断,而不是无限跑。这五条是我走过不少弯路之后归纳出来的。后面所有章节,本质上都在围绕这五条展开。2. 底层抽象:一个 Runtime 究竟在编排什么如果让我用一张概念图概括 Runtime,我会画这几个元素:Agent、Task、Tool、Message、Context、Memory。Runtime 做的事情,就是让这些元素在运行期以一种有序的方式流动起来。Agent 是执行主体,拥有模型配置、系统提示词、可用工具集。Task 是一次具体的执行单元,包含输入参数、目标说明、执行预算。Tool 是 Agent 可以调用的外部能力,本质是一个带描述和参数模式约束的函数。Message 是模型与工具之间传递信息的载体。Context 是当前运行过程中的全部状态快照,包括历史消息、变量、工具结果。Memory 是跨会话的记忆存储,解决“下次还能记住”的问题。很多工程问题都源于这些概念之间的边界不清晰。比如把 Context 和 Memory 混在一起用,导致上下文越来越长,费用越来越高,模型表现反而越来越差。这两个概念分开讲,后面会专门展开。2.1 运行循环是心脏,所有扩展都挂在主线上如果让我用伪代码描述 Runtime 最核心的骨架,大概是这样一个循环:async def run_agent(agent, task): # 初始化上下文 context = Context.from_task(task) step = 0 while step < agent.max_steps: # 1. 调用模型,得到结构化响应 response = await model.generate( messages=context.messages, tools=agent.tools.schemas(), temperature=agent.temperature, ) # 2. 记录模型响应 context.record_model_response(response) # 3. 判断是否有工具调用 calls = response.tool_calls if not calls: return context.final_output() # 4. 并发执行工具调用(可配置串行或并行) tool_results = await agent.bus.execute(calls, context) # 5. 将工具结果回填给上下文 context.record_tool_results(tool_results) step += 1 raise MaxStepExceeded(context)代码不长,但已经把运行的骨架勾勒清楚了。实际工程真正复杂的,是第 4 步背后的并发控制、上下文裁剪、错误恢复、临时变量保存这些细节。但底层逻辑不变——就是在“模型生成”和“工具执行”之间不断地做往复。强调一下,这个运行循环是 Agent Runtime 的心脏,也是后续所有能力扩展的挂载点。如果你要加入中断恢复、人工审批、异步任务,都应当在这条主循环上做扩展,而不是在外面包一堆 if 分支。很多团队做失败,就是没守住这条主线,各种特例全部堆在主循环外面,最后代码路径多到没人搞得清楚。2.2 模型层:决策调用和内容流要分开模型调用有两种基本模式。一种是一次性拿到完整响应,适合决策环节;另一种是流式响应,适合面向用户的聊天场景。Runtime 需要同时支持这两种,因为它们承担的功能不一样。决策环节通常用完整响应,因为它需要解析结构化的 tool_calls。聊天环节用流式响应,边生成边把文字推送给用户,体验更好。我踩过的一个坑,是把流式响应当成结构化决策的输入。结果每次都要等全部文字生成完再去解析,又慢又容易出错。在 Runtime 里,我会把“面向模型的决策调用”和“面向用户的内容流”分开两条通道,互不干扰。还有一个容易被忽略的点:不同模型服务的工具调用格式虽然类似,细节差异却很多。比如参数是否允许嵌套、工具名长度限制、系统提示词与工具信息的拼接顺序。一套好用的 Runtime 应当在模型适配层统一这些差异,对上层暴露相同的数据结构,而不是让业务代码针对每家服务写一堆分支。2.3 工具层:声明、校验、执行三阶段缺一不可工具是 Agent 能力的来源,也是 Runtime 最容易出问题的一层。我把工具接入拆成三个阶段:声明、校验、执行。声明阶段要编写工具的名称、描述、参数 JSON Schema。别小看这段描述,它对模型是否选择正确工具有决定性影响。一个清晰描述“在什么场景用、不接收什么参数、典型返回值是什么”的工具,比一句话描述好用太多。很多“模型不调工具”的诡异现象,最后查出来都是描述写得含糊。校验阶段需要把模型返回的参数和 Schema 做严格比对。模型经常返回多余的字段、缺字段、类型不符或者嵌套结构不对。这里不能手软,要在运行时做 JSON Schema 校验,校验不过的让模型重新生成,最多重试两三次,再做降级处理。执行阶段负责真实调用外部服务。几个必须遵守的原则:每个工具都要设置自己的超时;工具之间不能共享可变状态;工具返回结果要结构化,方便模型继续消费。还有一条经验:工具执行要做幂等设计。因为 Runtime 在恢复任务时可能重放工具调用,如果工具本身有副作用,必须保证重复执行不会产生坏影响。2.4 状态层:运行状态、会话状态、长期记忆要分开状态管理是 Runtime 设计里最容易模糊的地方。我习惯拆成三类:运行状态(Runtime State):当前任务内部的数据,包括循环计数、中间变量、待处理的工具调用。任务结束就回收或归档。会话状态(Session State):一次用户会话的持久化数据,包括历史消息、用户偏好、任务结果。跨多个任务存在。长期记忆(Long-term Memory):跨会话的摘要知识,通常以向量或结构化知识形式存储,由专门模块维护。很多人把这三类全塞进同一个“上下文”里,结果上下文越来越臃肿。正确做法是:运行状态放内存或工作区,会话状态持久化到存储,长期记忆由异步模块周期性写入。这样既能控制单次上下文的长度,也能保证恢复时有据可依。这里省不了事,省事的结果就是后续每轮都要为“上下文太长”买单。3. 编排模型与执行引擎的关键机制编排模型指的是 Agent 以什么方式组织自己的思考、行动和计划。业界常见的无非 ReAct、Plan-and-Execute、Graph 编排、事件驱动编排。Runtime 一般要支持多种,因为不同任务适合不同模型。3.1 常见编排模型的对比与选型ReAct 模式:思考-行动-观察循环,每一步基于上一次结果推进。适合带工具的问答、客服、信息检索。Plan-and-Execute:先生成计划,再逐步执行,最后汇总。适合多步骤数据处理、项目型任务。Graph/状态机编排:把节点和边显式定义出来,Agent 只能在预设路径上运行。适合流程固定、风险敏感的场景,比如审核流程。事件驱动:用消息队列解耦 Agent 之间的通信,适合多智能体协作场景。我实际选型的经验是:凡是流程相对固定的业务,尽量用 Graph;开放式问题用 ReAct;需要长周期计划的任务用 Plan-and-Execute。不要把什么都套进“通用智能体”,指望一个模式解决所有问题,那样反而会失控。3.2 Runtime 循环要不要做成状态机一说到编排,很多人第一反应是“状态机”。我的观点是:不要让所有 Agent 都走一个显式的状态机,而是让 Runtime 的运行循环本身具备状态机的核心能力,再向用户暴露状态转移的钩子。具体来说,Runtime 内部维护一个运行阶段枚举:初始化、生成、工具执行、后处理、完成、失败。外部可以在每个阶段前后挂接回调,比如“生成前检查 Token 预算”“工具执行后保存快照”。这样既保持循环的灵活性,又提供确定性控制的抓手。加了状态字段以后,还有一个额外的好处:能支持人工审批。审批本质上是“暂停-等待外部事件-恢复”的过程,有状态字段后实现起来很简单。有一次我给流程系统加人工审核,就靠这个状态机制,半天就改完了,完全没推翻主线逻辑。3.3 并发、并行、限流与优先级Agent 执行过程中经常会遇到“一次要调多个工具”的情况。比如分析一份文档,同时需要检索资料、计算指标、读取结构化数据。这三个调用互相独立,应该并行。Runtime 要支持工具组的并发执行,并且对并行数量做上限。我常用的配置是:全局信号量控制并发工具数,默认 4-8 个;单个 Agent 循环步数默认 15 步;单任务 Token 预算按模型区分,比如输入 4 万、输出 8 千。并发工具调用结束后,按固定顺序把结果拼回上下文,否则模型每次看到的顺序不同,行为不稳定。限流方面,除了模型服务本身的 API 限流外,Runtime 还需要业务级限流。比如高优先级任务不能因为后排队的任务挤占资源,就需要优先级队列。轻量做法是给任务加 priority 字段,调度器按优先级和入队时间排序,同一优先级先到先得。3.4 异常、重试、超时和 Token 预算的统一策略模型调用和工具调用都会失败,失败策略应该在 Runtime 统一处理,而不是散落在业务代码里。我的默认策略是这样一套:单次模型调用超时:30 秒,超时后重试 1 次,再失败就抛任务级错误。工具调用超时:按工具配置,默认 10 秒,允许重试 1 次,重试仍失败就把错误信息作为工具结果返回给模型,让模型自己决定下一步。整体任务超时:单任务累计执行不超过 10 分钟,超时强制中断。Token 预算:每次模型请求前检查累计消耗,超了就先裁剪历史消息,还是超就终止。这里最值得展开的是“把工具错误信息返回给模型”的设计。因为模型能看到工具报错的原因,很多时候自己会换一个工具或者换一种参数,不需要人介入。这样比直接中断任务、留人工排查要高效得多。当然,要设置重试上限,不能让同一个错误反复消耗 Token。4. 工程落地:从骨架到可维护系统的实践前面讲的是原理,这一章落到工程。这部分不看概念多漂亮,就看崩不崩得住。我也是踩过坑才把模块逐步拆干净的。4.1 模块边界:编排层、执行层、工具层、存储层我不会把 Runtime 做成一个大单体,而是拆成四个模块:编排层(Orchestrator):负责主循环、阶段转移、策略注入。所有任务都从这里进出。执行层(Executor):负责模型调用、工具执行的并发调度和生命周期管理。工具层(Tool Registry):负责工具注册、发现、参数校验、调用封装。存储层(Store):负责消息、任务状态、执行记录的持久化。模块之间通过接口通信,而不是直接共享对象。比如编排层只知道工具层提供了注册表,但不关心工具具体实现。存储层提供一个“记录任务事件”的接口,编排层往里写,但不关心底层是数据库还是对象存储。这样带来的直接好处是:单元测试容易,替换存储不影响业务逻辑。实际开发中我遇到过不少团队,把工具调用直接写在编排层里,看起来少了一层,但新加一个工具就要改编排层代码,后面演进非常痛苦。拆层一定要坚决,哪怕前期多写几个接口定义。4.2 可观测性是 Runtime 的生命线强烈建议给 Runtime 设计两类观测能力:一类是全局 trace,一类是事件日志。全局 trace 记录每个任务的调用链:从用户输入开始,到每次模型调用、每个工具执行、每次上下文更新,形成一棵树。排查“模型为什么没走预期分支”时,只需要看 trace 就能还原整个过程。事件日志则是扁平的结构化记录,每一条包含任务 ID、阶段、时间戳、关键参数、Token 消耗。两组配合,基本覆盖线上绝大多数问题。另外要记录 Token 和成本维度。每个任务结束时要输出总 Token、各环节 Token 分布、工具调用次数、耗时。把这些数据汇总起来,才能知道 Agent 到底贵不贵、瓶颈在哪里。我之前给某项目做成本分析,发现 70% 的 Token 都花在重复读取历史消息上,后来加了摘要机制,成本直接降了一半。这个例子说明,观测不只是用来查 bug,还能指导架构优化。4.3 持久化与恢复:断点续跑不是可选项生产环境的 Agent 执行经常要跑很久,尤其是有外部依赖的时候。进程不崩属于运气,会崩才是常态。Runtime 要有两个恢复能力:任务恢复和会话恢复。任务恢复是指 Task 执行过程中,如果进程挂掉,重启之后根据持久化的运行状态继续执行。这就要求运行状态至少每步执行后落盘一次。落盘频率过高会影响性能,通常每个关键阶段落一次就可以:生成完成、工具执行完成、任务结束。工具的执行结果必须记录,避免恢复后重放。这里又回到工具幂等性的必要性:重放不可怕,可怕的是重放产生副作用。会话恢复指的是用户下次发起对话时,能从存储里加载历史会话。我的经验是持久化消息和摘要并行:完整消息用于精确回滚,摘要用于长对话的上下文裁剪。只有摘要,恢复精度不够;只有完整消息,上下文管理就缺了抓手。4.4 测试能力:没有 Mock,就没有快乐的开发Agent 应用测试往往被忽视。模型返回不稳定,工具调用有副作用,自动化测试很容易写得别扭。但不测试,线上一定会出事。我把测试拆成三层:单测层:用 Mock 模型(固定返回预设文本或 tool_calls)来测编排逻辑分支。运行循环的每个分支都能稳定覆盖。集成层:用真实模型服务,但工具是可控的 Fake 工具,验证真实模型 + 虚拟工具下的行为是否符合预期。评估层:准备一组基准用例集,每条有预期结果或关键检查点,批量跑完看通过率。这是上线前最重要的把关。我有个印象很深的教训:某项目只做了单测,没做评估层,结果改了一次提示词后,一个关键流程的准确率掉了十几个点,竟然没发现。后来加了评估层,每次改动都跑一遍基准集,再没犯过这种低级错误。评估层初期建设成本不低,但它的价值会随着模型和提示词迭代次数增加而放大。5. 我踩过的坑和排查实录这一章专门分享实际工程中遇到的典型问题。每一条都花过不少时间排查,写下来帮大家少走弯路。5.1 “模型一直不调用工具”的三种原因与对策遇到这种问题,最先怀疑的不该是模型能力,而是工具描述。工具描述和系统提示词存在冲突时,模型会优先服从系统提示词,忽略工具。比如系统提示词说“不要调用工具,直接回答”,那工具写得再好也没用。其次,工具 Schema 中必填参数设置不合理。最常见的是把某个参数设为必填,但模型根本没有相关信息,无法生成合法参数,于是干脆不调工具,直接回答“我没有权限获取”。解决方式是把参数改为可选,或者直接拿掉,让模型自行决定。最后还有一种隐蔽原因:模型返回了 tool_calls,但 Runtime 解析失败了,错误被吞掉。表面看模型“没调用”,实际是调了但没成功。遇到这种情况,要把 Runtime 层的解析错误完整打印出来,很快就能发现是工具名带了空格,还是 JSON 层级不对。5.2 上下文膨胀和记忆过载Agent 跑几轮之后,历史消息不断增长,很快会触碰上下文窗口限制。早期图省事,把所有历史塞进去,结果模型越来越迟钝,输出质量明显下降,账单还吓人。我的解决方案是两层裁剪。第一层是摘要,早期对话压缩成概要,只保留最近几轮完整消息。第二层是结构化压缩,工具返回的长文本预处理为摘要或统计值,不直接进上下文。另外还加了“上下文健康度”指标,预估 Token 数和窗口占比,超过阈值就强制触发裁剪。这条值得强调:上下文管理不是“省 Token”这么简单,它直接影响模型注意力分布和输出质量。上下文越长,模型越容易在无关信息里打转。5.3 并发工具调用引发的乱序问题并行执行工具后,如果回填顺序不一致,模型看到的世界就可能是乱的。比如两个工具都修改了同一个变量,后返回的先回填,顺序就乱了,模型基于混乱的现场做决策,结果自然不稳定。解决方式是工具分组和排序策略。并行组内工具可以乱序执行,但回填时必须按预定义顺序统一写回。最好给每个工具结果带上 execution_order 字段,这样即使在恢复重放时也能保持顺序。这个细节看起来不起眼,实际影响很大。注意并发越高的系统,越需要严格遵守回填顺序。5.4 常见问题速查表现象可能原因排查方向解决方案模型不调用工具提示词与工具描述冲突检查系统提示词与工具描述调整描述,检查 Schema 必填项工具参数频繁校验失败Schema 定义不合理查看模型返回的原始 tool_calls放宽参数约束,增加默认值任务执行到一半中断工具超时未处理查看工具调用日志设置超时,配置重试策略恢复后行为不一致状态未持久化/工具未幂等检查运行状态落盘时机关键阶段落盘,工具幂等设计Token 消耗过高历史消息未裁剪分析成本分布摘要机制,结构化压缩并行结果乱序回填顺序不稳定检查执行器回收顺序固定回填顺序模型频繁输出解析错误模型能力与响应格式不匹配查看原始响应换模型或加格式约束示例还有一条备忘:排查任何 Agent 运行问题时,第一步永远是看分层的日志,而不是从头读代码。只要 trace 和事件日志齐全,大部分问题的定位时间能从小时级降到分钟级。6. 构建 Runtime 阶段的一些体会最后聊一点不那么技术、但很实际的东西。很多人觉得好用的 Runtime 应该功能越多越好,但我做了一年半载之后留下的教训是:稳定性和可观测性永远排在功能前面。我的默认原则是:先做一个只支持 ReAct 循环的最小 Runtime,跑通一个真实任务;在此基础上逐步加入并行、恢复、评估、成本控制。功能越多,出问题的点就越多,没有足够的观测手段之前,一味堆功能只会连“哪里出了问题”都看不到。另外还有一条建议:Runtime 一定要早点和业务解耦。最痛苦的版本是 Runtime 和某个特定业务绑得非常紧,业务一改,Runtime 也要跟着改,开发最后都不敢懂那块代码。正确的方向是,把业务知识放在工具和提示词配置里,把 Runtime 做成通用的、可复用的底层。我最近把这套能力沉淀成了团队内部的基础服务,给其他项目组使用。只要做到“新 Agent 十几分钟接入、默认自带观测和限流、上线后出问题能快速定位”,团队就有动力去用。这条路径已经在我参与的几个项目中验证过,效果比让每个项目自己从零搭一套好得多。对于一个团队来说,Runtime 最终拼的不只是技术实现,更是接入体验和长期维护的效率,这两点一定不要等做完了才想。
做了几年 Agent 相关系统,我有一个越来越强烈的感受:现在缺的不是能跑通 Demo 的代码,而是一套真正经得起生产环境考验的 Agent Runtime。很多人把“编排”想得很玄,觉得只要模型够聪明,循环调用工具就行。但真实工程里,模型返回不稳定、工具调用有副作用、进程随时可能崩、Token 成本还会失控,这些问题不靠 Runtime 层面的设计去兜住,业务代码迟早变成一团乱麻。这篇内容我想完整梳理一下 Agent 编排的全景:底层抽象怎么划、运行循环怎么设计、状态和记忆怎么管、工程落地要补哪些基础设施,以及我踩过的那些坑。核心目标是帮大家理解一件事——什么是“好用”的 Agent Runtime,以及怎么从零把它搭起来。适合正在做 Agent 应用、想把零散代码沉淀成平台能力的工程师阅读,也适合准备做 Agent 基建的团队做参考。1. 先搞清楚 Agent Runtime 到底在解决什么问题要谈 Runtime,先得承认一个现实:Agent 应用和传统应用有本质差异。传统程序的执行路径是确定的,if-else 分支写得再复杂,每一步都是预设好的;Agent 的路径是模型实时生成的,同样的输入,这次可能先调 A 工具再调 B,下次可能直接给出答案。你没法预先画出每一条调用链,只能提供足够的约束、观测手段和容错能力,让模型在可控范围内自由发挥。“编排(Orchestration)”这个词,指的就是对这条动态路径的控制与承载。它在解决的核心问题是:模型循环 + 工具调用 + 状态管理 + 基础设施,这一整套公共能力要沉淀成独立层,让业务开发只关心 Agent 的目标、工具和策略,而不必重复实现循环、并发、重试这些脏活。我见过不少失败的项目,模型能力其实没问题,但工程侧一塌糊涂。出现过的最典型现象,就是业务代码退化成一大锅“胶水代码”。明明只是调几个工具,代码里却挤满了模型调用、JSON 解析、异常捕获、上下文拼接、日志输出。一旦任务链路变长,变量满天飞,中间结果靠局部变量传来传去,想加一个新工具都要担心会不会影响全局。Pexels 有一位同事的项目就是这样,把一个多轮调用的 Agent 写到 2000 多行,最后所有逻辑纠缠在一起,改一个参数会牵连到好几个分支,只能重写。这个场景不是个例,而是很多人迟早要撞上的墙。独立出 Runtime 层,本质是把复杂度降下来:让该稳定的地方稳定,该灵活的地方灵活。1.1 Runtime 的边界:该管什么、不该管什么我倾向于把 Runtime 的职责收窄为四件事:调度、状态、资源、观测。调度:负责模型循环的推进,包括要不要继续、下一步调哪个工具、执行完工具后如何把结果反馈给模型。状态:负责维护会话上下文、变量、临时结果,以及和持久化存储之间的同步。资源:负责并发控制、限流、超时、Token 预算、工具执行配额。观测:负责记录运行轨迹、耗时指标、Token 消耗和成本分摊。在这四件事之外,比如 Agent 的目标怎么设计、工具怎么实现、提示词怎么写,这些不应该放进 Runtime。好的 Runtime 要尽量保持通用性,把业务差异交给上层配置和工具层。我见过一些团队把业务规则直接写死在编排引擎里,光看前期是快,但换一个场景就要改引擎,最后谁也不敢动核心代码,这比业务胶水更可怕。1.2 评估 Runtime 好不好的五个维度我用来判断一套 Runtime 是否“好用”的标准只有五条:接入成本:新写一个 Agent 需要多少样板代码。如果声明好工具、写好目标,几十行就能跑起来,说明抽象合理。可观测性:是否天然产出结构化日志和 trace,不需要业务代码额外埋点。容错能力:模型输出不规范、工具超时、网络抖动,这些是否都有默认策略。可扩展性:能否在不修改 Runtime 的情况下,加入新的工具类型、记忆策略、编排模型。运行成本可控:能否对每次任务做 Token 和耗时预算,超了就中断,而不是无限跑。这五条是我走过不少弯路之后归纳出来的。后面所有章节,本质上都在围绕这五条展开。2. 底层抽象:一个 Runtime 究竟在编排什么如果让我用一张概念图概括 Runtime,我会画这几个元素:Agent、Task、Tool、Message、Context、Memory。Runtime 做的事情,就是让这些元素在运行期以一种有序的方式流动起来。Agent 是执行主体,拥有模型配置、系统提示词、可用工具集。Task 是一次具体的执行单元,包含输入参数、目标说明、执行预算。Tool 是 Agent 可以调用的外部能力,本质是一个带描述和参数模式约束的函数。Message 是模型与工具之间传递信息的载体。Context 是当前运行过程中的全部状态快照,包括历史消息、变量、工具结果。Memory 是跨会话的记忆存储,解决“下次还能记住”的问题。很多工程问题都源于这些概念之间的边界不清晰。比如把 Context 和 Memory 混在一起用,导致上下文越来越长,费用越来越高,模型表现反而越来越差。这两个概念分开讲,后面会专门展开。2.1 运行循环是心脏,所有扩展都挂在主线上如果让我用伪代码描述 Runtime 最核心的骨架,大概是这样一个循环:async def run_agent(agent, task): # 初始化上下文 context = Context.from_task(task) step = 0 while step < agent.max_steps: # 1. 调用模型,得到结构化响应 response = await model.generate( messages=context.messages, tools=agent.tools.schemas(), temperature=agent.temperature, ) # 2. 记录模型响应 context.record_model_response(response) # 3. 判断是否有工具调用 calls = response.tool_calls if not calls: return context.final_output() # 4. 并发执行工具调用(可配置串行或并行) tool_results = await agent.bus.execute(calls, context) # 5. 将工具结果回填给上下文 context.record_tool_results(tool_results) step += 1 raise MaxStepExceeded(context)代码不长,但已经把运行的骨架勾勒清楚了。实际工程真正复杂的,是第 4 步背后的并发控制、上下文裁剪、错误恢复、临时变量保存这些细节。但底层逻辑不变——就是在“模型生成”和“工具执行”之间不断地做往复。强调一下,这个运行循环是 Agent Runtime 的心脏,也是后续所有能力扩展的挂载点。如果你要加入中断恢复、人工审批、异步任务,都应当在这条主循环上做扩展,而不是在外面包一堆 if 分支。很多团队做失败,就是没守住这条主线,各种特例全部堆在主循环外面,最后代码路径多到没人搞得清楚。2.2 模型层:决策调用和内容流要分开模型调用有两种基本模式。一种是一次性拿到完整响应,适合决策环节;另一种是流式响应,适合面向用户的聊天场景。Runtime 需要同时支持这两种,因为它们承担的功能不一样。决策环节通常用完整响应,因为它需要解析结构化的 tool_calls。聊天环节用流式响应,边生成边把文字推送给用户,体验更好。我踩过的一个坑,是把流式响应当成结构化决策的输入。结果每次都要等全部文字生成完再去解析,又慢又容易出错。在 Runtime 里,我会把“面向模型的决策调用”和“面向用户的内容流”分开两条通道,互不干扰。还有一个容易被忽略的点:不同模型服务的工具调用格式虽然类似,细节差异却很多。比如参数是否允许嵌套、工具名长度限制、系统提示词与工具信息的拼接顺序。一套好用的 Runtime 应当在模型适配层统一这些差异,对上层暴露相同的数据结构,而不是让业务代码针对每家服务写一堆分支。2.3 工具层:声明、校验、执行三阶段缺一不可工具是 Agent 能力的来源,也是 Runtime 最容易出问题的一层。我把工具接入拆成三个阶段:声明、校验、执行。声明阶段要编写工具的名称、描述、参数 JSON Schema。别小看这段描述,它对模型是否选择正确工具有决定性影响。一个清晰描述“在什么场景用、不接收什么参数、典型返回值是什么”的工具,比一句话描述好用太多。很多“模型不调工具”的诡异现象,最后查出来都是描述写得含糊。校验阶段需要把模型返回的参数和 Schema 做严格比对。模型经常返回多余的字段、缺字段、类型不符或者嵌套结构不对。这里不能手软,要在运行时做 JSON Schema 校验,校验不过的让模型重新生成,最多重试两三次,再做降级处理。执行阶段负责真实调用外部服务。几个必须遵守的原则:每个工具都要设置自己的超时;工具之间不能共享可变状态;工具返回结果要结构化,方便模型继续消费。还有一条经验:工具执行要做幂等设计。因为 Runtime 在恢复任务时可能重放工具调用,如果工具本身有副作用,必须保证重复执行不会产生坏影响。2.4 状态层:运行状态、会话状态、长期记忆要分开状态管理是 Runtime 设计里最容易模糊的地方。我习惯拆成三类:运行状态(Runtime State):当前任务内部的数据,包括循环计数、中间变量、待处理的工具调用。任务结束就回收或归档。会话状态(Session State):一次用户会话的持久化数据,包括历史消息、用户偏好、任务结果。跨多个任务存在。长期记忆(Long-term Memory):跨会话的摘要知识,通常以向量或结构化知识形式存储,由专门模块维护。很多人把这三类全塞进同一个“上下文”里,结果上下文越来越臃肿。正确做法是:运行状态放内存或工作区,会话状态持久化到存储,长期记忆由异步模块周期性写入。这样既能控制单次上下文的长度,也能保证恢复时有据可依。这里省不了事,省事的结果就是后续每轮都要为“上下文太长”买单。3. 编排模型与执行引擎的关键机制编排模型指的是 Agent 以什么方式组织自己的思考、行动和计划。业界常见的无非 ReAct、Plan-and-Execute、Graph 编排、事件驱动编排。Runtime 一般要支持多种,因为不同任务适合不同模型。3.1 常见编排模型的对比与选型ReAct 模式:思考-行动-观察循环,每一步基于上一次结果推进。适合带工具的问答、客服、信息检索。Plan-and-Execute:先生成计划,再逐步执行,最后汇总。适合多步骤数据处理、项目型任务。Graph/状态机编排:把节点和边显式定义出来,Agent 只能在预设路径上运行。适合流程固定、风险敏感的场景,比如审核流程。事件驱动:用消息队列解耦 Agent 之间的通信,适合多智能体协作场景。我实际选型的经验是:凡是流程相对固定的业务,尽量用 Graph;开放式问题用 ReAct;需要长周期计划的任务用 Plan-and-Execute。不要把什么都套进“通用智能体”,指望一个模式解决所有问题,那样反而会失控。3.2 Runtime 循环要不要做成状态机一说到编排,很多人第一反应是“状态机”。我的观点是:不要让所有 Agent 都走一个显式的状态机,而是让 Runtime 的运行循环本身具备状态机的核心能力,再向用户暴露状态转移的钩子。具体来说,Runtime 内部维护一个运行阶段枚举:初始化、生成、工具执行、后处理、完成、失败。外部可以在每个阶段前后挂接回调,比如“生成前检查 Token 预算”“工具执行后保存快照”。这样既保持循环的灵活性,又提供确定性控制的抓手。加了状态字段以后,还有一个额外的好处:能支持人工审批。审批本质上是“暂停-等待外部事件-恢复”的过程,有状态字段后实现起来很简单。有一次我给流程系统加人工审核,就靠这个状态机制,半天就改完了,完全没推翻主线逻辑。3.3 并发、并行、限流与优先级Agent 执行过程中经常会遇到“一次要调多个工具”的情况。比如分析一份文档,同时需要检索资料、计算指标、读取结构化数据。这三个调用互相独立,应该并行。Runtime 要支持工具组的并发执行,并且对并行数量做上限。我常用的配置是:全局信号量控制并发工具数,默认 4-8 个;单个 Agent 循环步数默认 15 步;单任务 Token 预算按模型区分,比如输入 4 万、输出 8 千。并发工具调用结束后,按固定顺序把结果拼回上下文,否则模型每次看到的顺序不同,行为不稳定。限流方面,除了模型服务本身的 API 限流外,Runtime 还需要业务级限流。比如高优先级任务不能因为后排队的任务挤占资源,就需要优先级队列。轻量做法是给任务加 priority 字段,调度器按优先级和入队时间排序,同一优先级先到先得。3.4 异常、重试、超时和 Token 预算的统一策略模型调用和工具调用都会失败,失败策略应该在 Runtime 统一处理,而不是散落在业务代码里。我的默认策略是这样一套:单次模型调用超时:30 秒,超时后重试 1 次,再失败就抛任务级错误。工具调用超时:按工具配置,默认 10 秒,允许重试 1 次,重试仍失败就把错误信息作为工具结果返回给模型,让模型自己决定下一步。整体任务超时:单任务累计执行不超过 10 分钟,超时强制中断。Token 预算:每次模型请求前检查累计消耗,超了就先裁剪历史消息,还是超就终止。这里最值得展开的是“把工具错误信息返回给模型”的设计。因为模型能看到工具报错的原因,很多时候自己会换一个工具或者换一种参数,不需要人介入。这样比直接中断任务、留人工排查要高效得多。当然,要设置重试上限,不能让同一个错误反复消耗 Token。4. 工程落地:从骨架到可维护系统的实践前面讲的是原理,这一章落到工程。这部分不看概念多漂亮,就看崩不崩得住。我也是踩过坑才把模块逐步拆干净的。4.1 模块边界:编排层、执行层、工具层、存储层我不会把 Runtime 做成一个大单体,而是拆成四个模块:编排层(Orchestrator):负责主循环、阶段转移、策略注入。所有任务都从这里进出。执行层(Executor):负责模型调用、工具执行的并发调度和生命周期管理。工具层(Tool Registry):负责工具注册、发现、参数校验、调用封装。存储层(Store):负责消息、任务状态、执行记录的持久化。模块之间通过接口通信,而不是直接共享对象。比如编排层只知道工具层提供了注册表,但不关心工具具体实现。存储层提供一个“记录任务事件”的接口,编排层往里写,但不关心底层是数据库还是对象存储。这样带来的直接好处是:单元测试容易,替换存储不影响业务逻辑。实际开发中我遇到过不少团队,把工具调用直接写在编排层里,看起来少了一层,但新加一个工具就要改编排层代码,后面演进非常痛苦。拆层一定要坚决,哪怕前期多写几个接口定义。4.2 可观测性是 Runtime 的生命线强烈建议给 Runtime 设计两类观测能力:一类是全局 trace,一类是事件日志。全局 trace 记录每个任务的调用链:从用户输入开始,到每次模型调用、每个工具执行、每次上下文更新,形成一棵树。排查“模型为什么没走预期分支”时,只需要看 trace 就能还原整个过程。事件日志则是扁平的结构化记录,每一条包含任务 ID、阶段、时间戳、关键参数、Token 消耗。两组配合,基本覆盖线上绝大多数问题。另外要记录 Token 和成本维度。每个任务结束时要输出总 Token、各环节 Token 分布、工具调用次数、耗时。把这些数据汇总起来,才能知道 Agent 到底贵不贵、瓶颈在哪里。我之前给某项目做成本分析,发现 70% 的 Token 都花在重复读取历史消息上,后来加了摘要机制,成本直接降了一半。这个例子说明,观测不只是用来查 bug,还能指导架构优化。4.3 持久化与恢复:断点续跑不是可选项生产环境的 Agent 执行经常要跑很久,尤其是有外部依赖的时候。进程不崩属于运气,会崩才是常态。Runtime 要有两个恢复能力:任务恢复和会话恢复。任务恢复是指 Task 执行过程中,如果进程挂掉,重启之后根据持久化的运行状态继续执行。这就要求运行状态至少每步执行后落盘一次。落盘频率过高会影响性能,通常每个关键阶段落一次就可以:生成完成、工具执行完成、任务结束。工具的执行结果必须记录,避免恢复后重放。这里又回到工具幂等性的必要性:重放不可怕,可怕的是重放产生副作用。会话恢复指的是用户下次发起对话时,能从存储里加载历史会话。我的经验是持久化消息和摘要并行:完整消息用于精确回滚,摘要用于长对话的上下文裁剪。只有摘要,恢复精度不够;只有完整消息,上下文管理就缺了抓手。4.4 测试能力:没有 Mock,就没有快乐的开发Agent 应用测试往往被忽视。模型返回不稳定,工具调用有副作用,自动化测试很容易写得别扭。但不测试,线上一定会出事。我把测试拆成三层:单测层:用 Mock 模型(固定返回预设文本或 tool_calls)来测编排逻辑分支。运行循环的每个分支都能稳定覆盖。集成层:用真实模型服务,但工具是可控的 Fake 工具,验证真实模型 + 虚拟工具下的行为是否符合预期。评估层:准备一组基准用例集,每条有预期结果或关键检查点,批量跑完看通过率。这是上线前最重要的把关。我有个印象很深的教训:某项目只做了单测,没做评估层,结果改了一次提示词后,一个关键流程的准确率掉了十几个点,竟然没发现。后来加了评估层,每次改动都跑一遍基准集,再没犯过这种低级错误。评估层初期建设成本不低,但它的价值会随着模型和提示词迭代次数增加而放大。5. 我踩过的坑和排查实录这一章专门分享实际工程中遇到的典型问题。每一条都花过不少时间排查,写下来帮大家少走弯路。5.1 “模型一直不调用工具”的三种原因与对策遇到这种问题,最先怀疑的不该是模型能力,而是工具描述。工具描述和系统提示词存在冲突时,模型会优先服从系统提示词,忽略工具。比如系统提示词说“不要调用工具,直接回答”,那工具写得再好也没用。其次,工具 Schema 中必填参数设置不合理。最常见的是把某个参数设为必填,但模型根本没有相关信息,无法生成合法参数,于是干脆不调工具,直接回答“我没有权限获取”。解决方式是把参数改为可选,或者直接拿掉,让模型自行决定。最后还有一种隐蔽原因:模型返回了 tool_calls,但 Runtime 解析失败了,错误被吞掉。表面看模型“没调用”,实际是调了但没成功。遇到这种情况,要把 Runtime 层的解析错误完整打印出来,很快就能发现是工具名带了空格,还是 JSON 层级不对。5.2 上下文膨胀和记忆过载Agent 跑几轮之后,历史消息不断增长,很快会触碰上下文窗口限制。早期图省事,把所有历史塞进去,结果模型越来越迟钝,输出质量明显下降,账单还吓人。我的解决方案是两层裁剪。第一层是摘要,早期对话压缩成概要,只保留最近几轮完整消息。第二层是结构化压缩,工具返回的长文本预处理为摘要或统计值,不直接进上下文。另外还加了“上下文健康度”指标,预估 Token 数和窗口占比,超过阈值就强制触发裁剪。这条值得强调:上下文管理不是“省 Token”这么简单,它直接影响模型注意力分布和输出质量。上下文越长,模型越容易在无关信息里打转。5.3 并发工具调用引发的乱序问题并行执行工具后,如果回填顺序不一致,模型看到的世界就可能是乱的。比如两个工具都修改了同一个变量,后返回的先回填,顺序就乱了,模型基于混乱的现场做决策,结果自然不稳定。解决方式是工具分组和排序策略。并行组内工具可以乱序执行,但回填时必须按预定义顺序统一写回。最好给每个工具结果带上 execution_order 字段,这样即使在恢复重放时也能保持顺序。这个细节看起来不起眼,实际影响很大。注意并发越高的系统,越需要严格遵守回填顺序。5.4 常见问题速查表现象可能原因排查方向解决方案模型不调用工具提示词与工具描述冲突检查系统提示词与工具描述调整描述,检查 Schema 必填项工具参数频繁校验失败Schema 定义不合理查看模型返回的原始 tool_calls放宽参数约束,增加默认值任务执行到一半中断工具超时未处理查看工具调用日志设置超时,配置重试策略恢复后行为不一致状态未持久化/工具未幂等检查运行状态落盘时机关键阶段落盘,工具幂等设计Token 消耗过高历史消息未裁剪分析成本分布摘要机制,结构化压缩并行结果乱序回填顺序不稳定检查执行器回收顺序固定回填顺序模型频繁输出解析错误模型能力与响应格式不匹配查看原始响应换模型或加格式约束示例还有一条备忘:排查任何 Agent 运行问题时,第一步永远是看分层的日志,而不是从头读代码。只要 trace 和事件日志齐全,大部分问题的定位时间能从小时级降到分钟级。6. 构建 Runtime 阶段的一些体会最后聊一点不那么技术、但很实际的东西。很多人觉得好用的 Runtime 应该功能越多越好,但我做了一年半载之后留下的教训是:稳定性和可观测性永远排在功能前面。我的默认原则是:先做一个只支持 ReAct 循环的最小 Runtime,跑通一个真实任务;在此基础上逐步加入并行、恢复、评估、成本控制。功能越多,出问题的点就越多,没有足够的观测手段之前,一味堆功能只会连“哪里出了问题”都看不到。另外还有一条建议:Runtime 一定要早点和业务解耦。最痛苦的版本是 Runtime 和某个特定业务绑得非常紧,业务一改,Runtime 也要跟着改,开发最后都不敢懂那块代码。正确的方向是,把业务知识放在工具和提示词配置里,把 Runtime 做成通用的、可复用的底层。我最近把这套能力沉淀成了团队内部的基础服务,给其他项目组使用。只要做到“新 Agent 十几分钟接入、默认自带观测和限流、上线后出问题能快速定位”,团队就有动力去用。这条路径已经在我参与的几个项目中验证过,效果比让每个项目自己从零搭一套好得多。对于一个团队来说,Runtime 最终拼的不只是技术实现,更是接入体验和长期维护的效率,这两点一定不要等做完了才想。