从Agent到PTC:大模型工具调用的工程化落地实践

从Agent到PTC:大模型工具调用的工程化落地实践 最近几个月我一直在折腾大模型应用层的架构改造核心诉求就一句话把工具调用从“模型想调就调”变成“模型在程序编排下按规则调用”。这个过程中我对程序化工具调用PTC和动态工作流引擎的理解从最初看论文、看文档时的概念化认知慢慢落到了可以跑线上流量的实际代码。这篇文章想把这条架构演进的路线和实操经验完整梳理一遍重点聊聊 PTC 到底是什么、动态工作流引擎怎么设计、以及我们在生产环境踩过的那些坑。如果你正在纠结是直接上平台化 Agent 方案还是自研一套工作流编排这篇文章应该能给你一个相对清晰的判断依据。先说结论大模型工具调用这一两年演进非常快但从工程落地的角度看趋势是从“模型自主决策”逐步走向“程序化控制、模型按需执行”。PTC 不是银弹但它在可控性、可测试性、成本控制上比无脑 Agent 循环要靠谱得多。接下来我会从演进路线讲起再拆 PTC 的原理和代码实现最后整理一份动态工作流引擎的落地参考。1. 大模型工具调用的四阶段演进以及我们该停在哪个阶段1.1 最早期的做法用提示词硬约束模型输出 JSON在原生 Function Calling 出来之前想让大模型调用外部工具大家基本靠提示词工程。做法是在 system prompt 里塞一大段工具说明比如“当你需要查询天气时请输出{action: query_weather, args: {city: 北京}}”然后代码里用正则或者 JSON 解析去提取这段输出再分发到对应的函数。这个阶段最大的问题是输出不可控。模型偶尔会在 JSON 前后加一段解释性文字或者把参数名稍微改一下解析层就崩了。我们当时处理退款工单的场景模型经常把refund_amount输出成amount导致工具执行时直接报 KeyError。为了提升稳定性只能不断在提示词里加 few-shot 示例、加“不要输出任何多余内容”的威胁式指令实际效果还是很脆弱。这个阶段并不是完全没价值。它的好处是兼容一切模型哪怕是最老的开源模型只要会聊天就能勉强走通“模型输出指令、代码执行指令”的链路。但它本质上属于“文本协议解析”不适合任何需要稳定性的生产系统。我到现在还留着当时写的那套正则解析器每次看到都觉得自己当年真是在螺蛳壳里做道场。1.2 Function Calling 原生时代把工具调用变成接口能力后来各家大模型厂商陆续推出了结构化的工具调用接口也就是 Function Calling / Tool Calling。模型不再是“输出一段 JSON 文本让你去解析”而是直接在 API 返回体里给出结构化的tool_calls字段包含工具名和参数对象。这一下把稳定性拔高了一个量级。这个阶段我在项目里最直观的感受是调用参数基本不会漏了模型能严格按 JSON Schema 生成参数连嵌套对象都能正确填充。之前那套正则解析可以整个删掉代码大幅简化。而且由于工具调用和普通文本回复在 API 结构上区分开了判断“模型是想说话还是想调工具”变得非常明确if message.tool_calls:一行判断就完事。不过 Function Calling 解决的问题其实很有限它只保证“单次调用”的可靠性。真实业务里很少只调一个工具就结束。模型先调分类工具拿到类别再根据类别调不同的查询工具拿完结果还可能要对结果做进一步判断——这种多步、有依赖、有分支的流程Function Calling 本身并不负责。开发者得自己在外面写一个 while 循环不断把工具结果回传给模型让模型决定下一步调什么。这就引出了下一个阶段。1.3 平台化 Agent 的兴起让平台接管工具循环为了省去开发者自己写循环的麻烦主流云厂商推出了平台化 Agent 方案典型代表就是 Assistants API 这类产品。你不需要自己维护“模型输出 tool_call - 执行工具 - 回传结果 - 再触发模型”这个循环平台帮你把循环跑完。你只需要定义好工具列表然后等着收最终答案。这种方案对快速原型和 demo 来说确实爽。我记得第一次跑通平台化 Agent 时只写了不到 50 行代码就能让模型自己连续调两个工具完成一个跨库查询整个过程完全自动。这也导致当时团队内部一度觉得“再也不用写编排代码了”。但项目一上线问题就来了。平台把循环封装成黑盒之后过程变得不可控、不可观测。模型自己在什么条件下决定调哪个工具、调错了怎么办、某一步出错了怎么重试、中间态怎么恢复这些平台要么不暴露要么提供了很粗粒度的配置项。而且平台本身是强绑定的一旦用了就基本上没法在模型和工具之间插入自定义逻辑。当你需要定制一个“如果工具A返回风险等级高就走人工审批”这种简单业务规则时平台方案就非常别扭。1.4 演进的转折点工程确定性压倒一切经历了几个项目的反复摇摆之后我意识到一个很核心的问题业务方对大模型的期待是“智能”但对工程系统的要求是“确定性”。你可以在可控范围内给模型一定的自主空间但流程里哪个节点走什么分支、什么条件下可以提前终止、哪些信息必须保留这些应该由程序来保证而不是靠模型临场发挥。这个认知转变直接把我推向了 PTC 的方向。所谓程序化工具调用就是在不放弃大模型语义理解能力的前提下把“调用链路的控制权”从模型手里拿回来交给代码。模型负责它擅长的部分——理解意图、生成参数、归纳结果程序负责它擅长的部分——分支判断、重试控制、状态管理、成本约束。这样即保留了大模型的灵活性又保证了工程系统的可控性。我把这四个阶段的区别整理成了一个对照表方便你快速定位自己的项目正处于哪个阶段阶段控制权归属稳定性可控性开发成本适用场景提示词约束 JSON文本协议低低低后期维护高原型验证、不能接入专用 API 的场景Function Calling模型中中中单次工具调用的业务平台化 Agent平台中低极低快速 demo、工具少且流程简单PTC 动态工作流程序高高较高前期生产级多步骤、多分支工具编排2. PTC 核心原理拆解把决策权从模型交还给程序2.1 PTC 到底改了什么决策者变了PTC全称 Programmatic Tool Calling程序化工具调用。很多人第一次听到这个概念会误以为它只是“让程序循环调用工具”和之前自己写 while 循环没区别。其实关键区别在于决策者变了。传统循环写法是模型输出 tool_call - 程序执行 - 把结果回传给模型 - 模型再决定下一步。决策者始终是模型程序只是跑腿的执行者。而 PTC 的做法是模型输出的 tool_call 只是给程序的一个“建议”程序拿到建议之后要结合自己的业务规则、当前状态、历史记录决定是否执行这个调用、是否修改参数、是否跳过该操作直接走另一个分支。举个具体例子。用户说“我退款申请已经提交三天了还没到账”。传统循环下模型大概率会直接调“查询退款状态”工具。但如果在程序化控制下程序可以先判断用户身份VIP 还是普通用户、再判断是否已经触发过人工核实、是否在敏感时间段这些规则完全不需要经过模型代码直接就能决定。模型不懂你的业务风控策略它只知道“用户想查退款状态”而你知道“VIP 用户超过 48 小时未到账必须优先人工介入”。所以这个决策必须由程序来做。2.2 三个关键能力直接调用、流式传入、交叉调用PTC 在接口能力上主要做了三个层面的升级这三个能力我都在实际项目里验证过确实能实质性地提升工具调用的效率。第一个是直接调用。程序可以直接触发一个工具调用而不需要先让模型“意识到需要调用某个工具”。比如用户进来先校验 token、查询用户等级这类前置操作根本不需要大模型参与程序直接调工具拿到结果再把这个结果作为上下文的一部分传给模型。这减少了不必要的模型往返省 token 也省延迟。第二个是流式传入工具调用结果。传统的做法是工具执行完把完整结果全部塞回给模型。PTC 支持把工具调用结果以流式方式逐块传给模型。这在工具返回内容特别长时很有效比如数据库查询返回了几百行记录你可以让模型边接收边处理不需要等全部数据到达才开始生成。首次 token 时间能明显降低。第三个是交叉调用。多个工具调用可以交叉执行之前的工具调用还没有完全跑完后续的工具调用已经可以开始并且部分结果可以先交给模型处理。比如同时调“用户行为分析”和“库存查询”行为分析先返回这部分结果就可以先喂给模型做预判等库存信息到了再补齐最终答案。整体耗时从“串行求和”优化成“最大者加少量损耗”。2.3 为什么说 PTC 是“把循环交给代码”这里有一个很容易被忽略的点PTC 并没有消灭“模型在循环中反复调用工具”这件事而是把“循环的控制逻辑”从隐式的自然语言交给了显式的代码。传统平台化 Agent 里循环逻辑是平台内置的你只能通过调整 prompt 来间接影响模型的行为比如“如果工具返回结果不满足要求再调一次 XX 工具”。这种提示词级别的控制非常弱模型可能理解不到位多调了一次、少调了一次都很难排查。而在 PTC 模式下循环的每一步都是代码里可见的你甚至可以写单元测试去验证“当分类工具返回 after_sales 时引擎是否确实调用了退款政策查询接口”。这种可测试性对于工程项目来说价值极大。我们现在对工作流里的每个路由策略都写了 pytest 测试覆盖面到了分支级别。模型升级导致行为变化时测试能第一时间暴露出差异。这在黑盒平台方案下几乎做不到。2.4 PTC 与 Function Calling 的区别一个管单次一个管全链路很多人会把 PTC 混淆成 Function Calling 的“增强版”实际上它俩不在一个维度上。Function Calling 解决的是“模型如何把一次调用意图转成结构化参数”。PTC 解决的是“整个调用链路如何被程序精细控制”。可以这样理解Function Calling 是给了模型一张“工具使用说明书”告诉它每个工具长什么样、参数怎么填。PTC 则是在说明书之外再加了一层“调度系统”决定说明书里的哪些步骤该执行、按什么顺序执行、遇到异常怎么处理。所以实际落地时PTC 通常是构建在 Function Calling 之上的。底层调用仍然走模型的 tool_calls 接口上层则是一个完全由程序控制的调度引擎。这套组合拳打下来才算把工具调用这个能力从一个“模型特性”变成了一套“工程基础设施”。3. 动态工作流引擎设计与一个能跑的工单分流示例3.1 静态工作流的局限流水线解决不了临场变卦聊完 PTC就得聊承载 PTC 的载体——工作流引擎。很多团队一开始用的是静态工作流也就是把流程画成 DAG有向无环图节点之间用条件边连接。比如“先调分类工具若返回 A 则走节点 2返回 B 则走节点 3”。这种设计在处理完全可枚举的业务规则时非常好用比如订单状态机。但放在大模型场景下就有点力不从心了。原因在于大模型工具调用的中间结果是不可穷举的。你很难在流程设计阶段就预料到“用户输入什么样的一句话会触发哪些工具组合”。比如工单分类返回“技术类”但你没法提前知道用户具体遇到的是 Linux 崩溃还是 Windows 蓝屏也就没法预先画好“崩溃”和“蓝屏”的所有下游分支。动态工作流引擎要解决的就是这个问题分支并非在设计期确定而是在运行期根据模型输出和工具结果实时计算。流程不再是黑板上一张画好的图而更像一个能即时生成路径的导航系统。3.2 动态工作流的分层设计状态、路由、工具三层分离我在项目里设计的动态工作流引擎分成三层这个分层我认为比较干净可以拿出来讲一下。第一层是状态层。整个引擎维护一个结构化的 state 对象所有工具的执行结果都会被写入 state后续路由决策只读取 state不直接读原始消息。比如分类结果写入state[category]后续判断基于这个值做分支。好处是状态可以被序列化、持久化任务中断后可以把 state 恢复出来继续跑。第二层是路由层。路由层负责决定“当前状态下该做什么”。它可以是规则代码也可以是“让大模型做决策、但决策范围被限定”的受限决策器。我倾向于把确定性规则优先放在代码里只有代码解决不了的时候才把决策交给模型。第三层是工具层。工具层只负责执行具体的工具调用不管业务逻辑。它接收参数、调外部接口、返回结果。工具不关心自己为什么被调用只关心输入输出。这个分层设计最大的收益是灵活。替换模型、替换工具、修改路由策略互不影响。我们后来把底层模型从 A 换到 B整个路由代码一行没改只是换了 client 配置。3.3 完整示例一个基于 PTC 的智能工单分流工作流下面给出一个可以直接跑通思路的最小实现场景是客服工单自动分流。用户提交工单后引擎先分类再根据分类结果决定走“退款政策查询”“技术知识库匹配”还是“人工转交”最后让模型基于工具结果生成最终答复。为了演示 PTC 的“程序化”特点代码里特意把路由判断写在引擎里而不是把“要不要调下一个工具”交给模型自己决定。import json import time from typing import Any, Dict, List, Optional # ---------- 工具注册表 ---------- TOOL_REGISTRY: Dict[str, Dict[str, Any]] {} def register_tool(name: str, description: str, parameters: dict): def decorator(func): TOOL_REGISTRY[name] { func: func, schema: { type: function, function: { name: name, description: description, parameters: parameters, }, }, } return func return decorator register_tool( classify_ticket, 对用户工单内容进行分类返回售后/技术/其他, { type: object, properties: { content: {type: string, description: 用户工单原文}, }, required: [content], }, ) def classify_ticket(content: str) - str: if any(k in content for k in (退款, 退货, 差价)): category after_sales elif any(k in content for k in (崩溃, 报错, 无法启动, 白屏)): category technical else: category other return json.dumps({category: category}, ensure_asciiFalse) register_tool( query_refund_policy, 查询不同用户类型的售后退款政策, { type: object, properties: { user_type: {type: string, enum: [standard, vip, reseller]}, }, required: [user_type], }, ) def query_refund_policy(user_type: str) - str: policies { standard: 普通用户 7 天内无理由退款超过 7 天需人工审核, vip: VIP 用户 15 天内无理由退款且免运费, reseller: 渠道商订单需联系专属售后经理处理, } return json.dumps({policy: policies.get(user_type, 未知用户类型)}, ensure_asciiFalse) register_tool( fetch_tech_articles, 根据报错关键词获取技术解决方案, { type: object, properties: { keyword: {type: string}, }, required: [keyword], }, ) def fetch_tech_articles(keyword: str) - str: db { 崩溃: 建议先清理缓存若问题复现请升级到 3.2.1 以上版本, 报错: 按错误码 5XXX 处理优先检查网络代理配置, 无法启动: 大概率是安装包损坏请重新下载完整包, } return json.dumps({articles: [db.get(keyword, 暂无匹配文档)]}, ensure_asciiFalse) register_tool( notify_human_review, 将工单转交人工客服处理, { type: object, properties: { ticket_id: {type: string}, reason: {type: string}, }, required: [ticket_id, reason], }, ) def notify_human_review(ticket_id: str, reason: str) - str: # 真实项目里这里会写入审核队列并触发企微/飞书通知 return json.dumps({status: dispatched, assignee: human-agent-group-3}, ensure_asciiFalse)工具注册完毕。接下来是引擎主体这个引擎把“状态管理”和“路由决策”放在显式的代码里而不是让模型自由决定一切class LLMClient: def __init__(self, base_url: str, api_key: str, model: str): self.base_url base_url self.api_key api_key self.model model def chat(self, messages: List[Dict], tools: Optional[List[Dict]] None) - Dict[str, Any]: # 这里对接任意兼容 Function Calling 的模型服务 # 实际项目用官方 SDK 或 requests 包返回体结构统一转换成 message 对象 raise NotImplementedError(替换为实际模型调用) class TicketWorkflowEngine: def __init__(self, llm: LLMClient): self.llm llm self.state: Dict[str, Any] {} self.messages: List[Dict] [] self.trace: List[Dict] [] def run(self, user_input: str, ticket_id: str, max_rounds: int 6) - str: self.state { ticket_id: ticket_id, user_input: user_input, category: None, tool_result: None, final_answer: None, } self.messages [{role: user, content: user_input}] for round_idx in range(max_rounds): message self.llm.chat(self.messages, toolslist(TOOL_REGISTRY.values())) self.messages.append(message) if not message.get(tool_calls): # 模型没有调用工具说明已经可以产出最终答案 self.state[final_answer] message.get(content, ) return self.state[final_answer] for call in message[tool_calls]: tool_name call[function][name] args json.loads(call[function][arguments]) tool_func TOOL_REGISTRY[tool_name][func] result execute_tool_with_retry(tool_func, args) self.messages.append({ role: tool, tool_call_id: call[id], content: result, }) self.trace.append({ round: round_idx, tool: tool_name, args: args, result: result, }) # ---------- 动态路由核心程序根据中间结果决定下一步 ---------- if tool_name classify_ticket: self.state[category] json.loads(result)[category] if self.state[category] after_sales: self._force_next_tool(query_refund_policy, {user_type: vip}) elif self.state[category] technical: self._force_next_tool(fetch_tech_articles, {keyword: self._extract_keyword()}) else: self._force_next_tool(notify_human_review, {...}) elif tool_name in (query_refund_policy, fetch_tech_articles): # 关键信息已拿到下一轮让模型基于结果生成最终答复 self.state[tool_result] result elif tool_name notify_human_review: self.state[final_answer] 工单已转交人工客服预计 2 小时内处理请留意通知。 return self.state[final_answer] return 超过最大处理轮次自动转人工队列 def _force_next_tool(self, tool_name: str, args: Dict): # 程序直接插入一个 tool_call绕开模型的主观决策 tool_schema TOOL_REGISTRY[tool_name][schema][function] self.messages.append({ role: assistant, content: None, tool_calls: [{ id: fcall_{int(time.time() * 1000)}, type: function, function: {name: tool_name, arguments: json.dumps(args, ensure_asciiFalse)}, }], force_insert: True, # 标记这是程序插入的调用不是模型生成的 }) def _extract_keyword(self) - str: # 实际实现可以简单用规则抽取也可以交给一个小分类模型 for word in (崩溃, 报错, 无法启动, 白屏): if word in self.state[user_input]: return word return 通用这段代码里_force_next_tool方法就是 PTC 的灵魂所在程序可以主动往对话流里插入一个工具调用不需要经过模型“思考”这一环。分类器返回售后类别后引擎直接决定调用退款政策查询而不是把决定权交还给模型。这就是“程序化控制工具调用”和“模型自主 Agent 循环”的最大区别。3.4 运行效果与扩展点用上面这个引擎跑一遍“客户说软件一直报错无法启动”的工单流程大致是引擎把用户原始输入发给模型模型看到有classify_ticket工具输出一次工具调用。引擎执行分类工具拿到categorytechnical。引擎不走模型直接插入fetch_tech_articles调用用规则抽取出的关键词“无法启动”去知识库匹配。工具返回解决方案引擎把结果作为 tool 消息追加到上下文中。模型基于工具结果生成最终答复“建议重新下载完整包大概率是安装包损坏。”整个流程里只有第 1 步和第 5 步用到了大模型的语义能力中间 2、3、4 步都是纯程序控制。这样做的好处是分类结果可控、路由逻辑可审计、成本也降下来了。如果要扩展成更通用的动态工作流引擎可以在state里多加一个next_actions队列每次工具执行完路由层往队列里塞动作引擎循环消费队列。这样多个工具可以并行入队、按优先级执行本质上就是一个由状态驱动的动态调度器。我们线上版本就是这么改的效果比在代码里写死 if-else 要好维护得多。4. 生产环境踩坑记录重试、并发、上下文与可观测性4.1 工具调用超时与幂等重试工具调用真正跑到生产环境第一个要面对的问题就是超时。上游接口网络抖动、数据库慢查询、第三方 API 限流任何一个环节出问题都会让工作流卡死。我们的重试策略经历了三轮迭代才稳定下来。第一版是无限重试结果上游服务雪崩时下游全在重试把故障放大了。第二版改成固定重试 3 次间隔 1 秒但没考虑幂等。退款类工单如果第一次调用已经创建了退款单只是响应超时重试就会重复创建。这里有个非常关键的教训涉及写操作的工具必须让工具实现方支持幂等键调用方在请求里带上唯一的 operation_id重试时用同一个 id上游才能去重。目前线上方案是指数退避 抖动 最大 3 次重试读操作可以放宽到 5 次写操作一律 2 次且必须带幂等键。这个配置跑了大半年没出过大问题。4.2 并行工具调用的乱序问题PTC 支持并行执行多个工具但并行意味着返回顺序不固定。我们最初把工具结果直接追加到 messages 列表里结果模型有时候会把人名和订单号对应错位。后来排查发现模型本身对 tool_call_id 的对应关系是能处理的真正的问题出现在我们自己的状态写入逻辑里A 工具和 B 工具同时写state[result]后写完的覆盖了先写完的。现在的规范是每个工具结果写入 state 时必须带工具名作为 key比如state[result][refund_query]、state[result][user_profile]。并行工具之间禁止共享可变字段。如果确实需要汇总两个结果再传给模型就在路由层加一个 merge 步骤负责把多个 key 合并成一个结构化对象再追加到 messages。4.3 上下文膨胀中间结果不能无脑回传工具调用每多一轮messages 就会膨胀一些。尤其数据库查询类工具几千行结果往里一塞几轮之后 token 消耗直接翻倍而且模型会被冗余信息干扰开始“一本正经地参考无关字段”。我们踩过一个大坑某个报表生成任务工具返回了接口原始响应里面全是调试字段模型看到status: OK就说任务执行成功实际业务字段根本没取到。后来定了一条硬性规则工具返回的数据必须先经过“结果适配层”清洗只保留模型需要的最小字段然后才允许追加到 messages。数据量特别大时还会先让一个小模型或规则脚本做摘要再传给主模型。这个改造让单任务平均 token 消耗降了约 40%效果非常明显。4.4 工具 Schema 的多模型兼容做工具注册表的时候尽量别直接绑定某一家的 schema 格式。不同模型对工具描述字段的长度限制、参数格式要求都不一样。有的模型要求 description 字段不能超过 1024 字符有的不支持anyOf、enum枚举有的对嵌套 object 的解析容易翻车。我们的做法是在注册表外面包了一层“Schema 适配器”根据不同模型自动转换格式。比如记录 schema 时用统一的自定义结构调模型前再转换成目标模型支持的格式。这样切换模型厂商时工具注册表完全不用改。这件事最好在第一版就做不然后面工具数量一多转换成本会非常高。4.5 可观测性救了我一命的那次线上故障有一次线上反馈工单回复质量突然下降很多应该转人工的高风险工单直接被模型自动处理了。看代码、看 prompt 都没问题最后是靠 trace 日志定位的某次发布把路由策略里的风控阈值参数读错了导致state[risk_level]一直是低风险。如果当时没有记录每一步的工具调用参数和状态变化这个问题可能还要排查很久。从那以后我们的 trace 里强制包含四件事每个节点的输入输出摘要、路由决策的依据、token 消耗统计、各工具调用耗时。每次发布新模型或改路由策略都要先跑一遍回放测试把线上历史工单用新配置重放一遍对比结果差异。这套可观测基础设施比任何架构文档都更能保证演进的安全性。5. 架构选型建议与落地路径参考5.1 平台化 Agent 与 PTC 动态工作流怎么选被问得最多的问题是我们到底该用平台化 Agent还是自研 PTC 动态工作流我一般会从三个维度来帮团队判断。第一个是流程复杂度和稳定性要求。工具少于 5 个、流程基本线性、对失败率不敏感直接用平台化 Agent 就行省时省力。工具超过 10 个、步骤之间有依赖和分支、每个分支还需要定制规则那 PTC 基本是必然选择。第二个是对过程可控的要求。业务需要审计“为什么这个工单走了人工、那个工单没走”需要指定某些高危操作必须人工确认后才执行这些只有程序化控制才能保证。模型自主决策的黑盒路径没法做严格承诺。第三个是成本敏感度。平台化 Agent 平台虽然开发快但 token 消耗往往更高因为模型需要在多轮循环里反复自主决策有时候会做一些冗余调用。PTC 把确定性逻辑摘出去后模型只干最核心的判断和生成长期跑下来成本差异非常可观。5.2 落地三步走从工具梳理到引擎上线如果你决定走 PTC 动态工作流这条路我的建议是不要一上来就写引擎代码先按三步走。第一步花一周左右把业务里所有工具枚举出来给每个工具写清楚输入输出、副作用类型读/写、依赖关系。这个表就是后面设计路由的基础。我们当时整理了 47 个工具按领域拆成 6 组才发现很多工具存在隐性依赖。第二步把路由规则画成表格而不是流程图。规则表格的行是条件列是动作比如“分类after_sales 且 用户等级vip - 调退款政策查询”。这种表格比流程图好维护得多也方便业务方直接参与评审。第三步把引擎做成“最小可用版本”先支持串行调用和 if-else 路由跑通一条完整业务路径后再逐步加并行、加状态持久化、加人工审批节点。一上来就想做通用编排引擎大概率会陷入抽象过度的泥潭。5.3 下一步演进方向多 Agent 协作与人工审批节点目前我们的引擎已经能支持简单的多 Agent 协作本质上是把每个 Agent 拆成一个“动态子工作流”由一个主路由决定把任务派发给哪个子工作流。这和单工作流的区别在于子工作流之间可能需要信息交换这时状态对象就要设计成可隔离、可合并的格式。另一个比较实用的演进方向是人工审批节点Human-in-the-Loop。具体做法是在路由规则里设置一个“审批门槛”当工具结果命中某些条件时工作流引擎不是继续往下执行而是把当前状态序列化推送到审批队列。人工通过后再把状态恢复回来继续跑。这需要状态层支持完整的序列化和恢复所以前面强调的“状态与逻辑分离”设计就变得非常重要。我个人在实际项目里最大的体会是PTC 这套体系的价值不在“自动化”而在“可控性”。它不是说要把大模型的能力关进笼子里而是让模型在它擅长的语义理解和生成任务上充分发挥同时把业务流程里那些需要确定性、可审计、可测试的部分留给程序。最初我们为了追求“智能”给模型加了大量自主权后来发现业务方真正满意的系统不是最聪明的而是最可预期的。如果你也在架构选型上犹豫我先给个最朴素的建议别急着写引擎先把工具清单和分支规则列成一张表这张表列完很多答案自然就清楚了。