news 2026/10/27 15:43:59

无Tool Calling的结构化通用Agent:从原理到工程实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
无Tool Calling的结构化通用Agent:从原理到工程实践

1. 为什么我决定把 Tool Calling 从 Agent 里拿掉

第一次看到"无 Tool Calling 的结构化通用 Agent"这个命题时,我的直觉是抗拒的。过去一年我搭过的 Agent 系统,几乎清一色依赖 Tool Calling:模型返回一个结构化的函数调用请求,运行时解析、执行、把结果塞回上下文,循环往复。这套范式成熟、稳定、生态完善,主流框架都把它当作一等公民。所以当有人提出"不用 Tool Calling 也能做通用 Agent"时,我第一反应是——这不是倒退吗?

真正让我改变想法的,是一次线上事故。我维护的一个内部 Agent 需要对接十几个外部能力,每个能力都注册成 Tool。某天上游模型供应商调整了函数调用相关的接口行为,返回的调用参数格式出现了细微偏差,整个链路直接崩了。排查花了大半天,最后发现根因不在我的代码,而在"模型原生 Tool Calling"这个黑盒上。那一刻我意识到:把 Agent 的核心控制流完全托付给模型厂商的函数调用实现,是一种隐性的强耦合。

于是我开始认真研究另一条路:让模型只输出纯文本,由我自己的代码去解析这段文本、决定下一步动作、执行、再拼回上下文。听起来像是回到了"用正则抠 JSON"的原始时代,但实际做下来,配合好的 Prompt 设计和结构化输出约束,这条路不仅可行,而且在可控性、可移植性、可调试性上反而更强。

这篇文章就是我这段时间实践的完整总结。它适合已经写过至少一个 Agent Demo、被 Tool Calling 的某些限制卡过、想搞清楚"结构化通用 Agent"到底怎么落地的人。如果你还在纠结 Agent 要不要上框架、Prompt 怎么写才稳、多轮循环怎么不跑飞,下面的内容应该能给你一些直接能抄的作业。

先明确一个概念边界。这里说的"无 Tool Calling",指的是不依赖模型厂商提供的原生函数调用能力(比如某些 API 里的 tools 参数、function_call 字段),而不是说 Agent 不能调用外部工具。恰恰相反,Agent 依然要调工具,只不过"什么时候调、调哪个、传什么参数"这件事,由模型用自然语言或约定格式表达出来,再由我们自己的运行时去解析和执行。这个区别很关键,后面所有设计都围绕它展开。

2. 结构化通用 Agent 的核心机制拆解

2.1 把"动作决策"从模型黑盒搬回自己的代码

原生 Tool Calling 的本质,是模型在生成时直接产出一个符合特定 schema 的结构化对象。好处是省事,坏处是这个 schema 的解析、校验、容错全在厂商手里,你只能被动接受。而无 Tool Calling 的思路是:模型只负责"想",运行时负责"解析和执行"。

具体来说,我让模型在每一轮输出里,用一段约定好的文本格式表达它的意图。最简单的形式是这样:

思考: 用户想知道今天的天气,我需要调用天气查询工具。 动作: get_weather 参数: {"city": "杭州", "date": "today"}

运行时拿到这段文本后,用解析器提取出"动作"和"参数",执行对应函数,再把结果作为新一轮的输入喂回去。整个控制流完全在我手里,模型换一家、接口改一版,只要它还能输出文本,我的 Agent 就不会崩。

这个设计的核心价值在于解耦。模型是"大脑",负责推理和决策;运行时是"脊髓",负责反射和执行。两者通过一份文本协议通信,而不是通过厂商私有的结构化通道。协议是我定的,我就能改、能扩展、能加校验。

2.2 文本协议为什么比 JSON 更适合做决策载体

很多人第一反应是:既然要结构化,为什么不直接让模型输出 JSON?我试过,结论是——纯 JSON 输出对模型不友好,对调试也不友好。

原因有三。第一,模型在生成 JSON 时容易在长文本里"漏括号""多逗号",尤其是参数里带中文或特殊字符时,解析失败率明显上升。第二,纯 JSON 没有"思考"的位置,模型被迫把推理过程也塞进字段里,反而让结构更乱。第三,出问题时你看到的是一坨没有语义的 JSON,很难快速定位模型到底哪一步想错了。

所以我采用的是**"思考 + 动作 + 参数"三段式文本协议**。思考部分用自然语言,允许模型自由发挥,这部分不参与解析,纯粹给人看、给模型自己"打草稿"用;动作部分是一个简短的标识符;参数部分才用类 JSON 的结构。这样既保留了推理的透明度,又让解析目标足够聚焦。

提示:思考段落不要省。实测下来,让模型先写一段思考再给动作,动作的准确率比直接输出动作高出一截。这相当于给模型一个"草稿纸",符合它自回归生成的特性。

2.3 ReAct 循环在无 Tool Calling 下的变形

经典的 ReAct 模式是"推理-行动-观察"三步循环。在原生 Tool Calling 下,"行动"这一步是模型直接吐函数调用;在无 Tool Calling 下,"行动"变成了模型吐文本,运行时解析后执行,再把"观察"结果拼回去。

这个变形带来一个额外好处:我可以在"观察"和下一轮"推理"之间插入自己的处理逻辑。比如对工具返回结果做裁剪、做摘要、做敏感信息过滤,甚至根据结果决定要不要触发一个子 Agent。原生 Tool Calling 的循环里,这些中间处理要么做不了,要么得绕很大一圈。

循环的终止条件也需要自己定义。我的做法是约定一个特殊的动作标识,比如finish,当模型输出这个动作时,把它的"参数"里的内容作为最终答案返回。这样整个循环的退出权也在运行时手里,而不是靠模型"自觉"停止。

2.4 通用性从哪来:动作注册表 + 统一解析器

"通用"这个词不是随便说的。一个无 Tool Calling 的 Agent 要做到通用,关键在于动作注册表和统一解析器这两块基础设施。

动作注册表就是一个字典,把动作名映射到具体的执行函数和参数校验规则。新增一个能力,只需要往注册表里加一条,不用改解析逻辑,也不用改 Prompt 的主体结构(顶多在系统提示里补一句这个动作是干嘛的)。统一解析器则负责把模型输出的文本稳定地拆成"思考/动作/参数"三部分,并对参数做类型转换和合法性检查。

有了这两块,Agent 的能力扩展就变成了"注册新动作"这么简单的事。我现在的项目里注册了二十多个动作,从查数据库到调外部接口到生成文件,全部走同一套协议,维护成本极低。

3. Prompt 设计:让模型稳定吐出可解析的结构

3.1 系统提示的骨架该怎么搭

无 Tool Calling 的 Agent,成败八成在 Prompt。模型不知道你要什么格式,它就会自由发挥,解析器直接歇菜。所以系统提示必须把"输出协议"讲得清清楚楚。

我的系统提示骨架大致分四块:角色定义、可用动作清单、输出格式规范、行为约束。角色定义一句话带过,告诉模型它是谁、要解决什么问题。可用动作清单是重点,每个动作要写清楚名字、用途、参数含义,最好给一个调用示例。输出格式规范要精确到分隔符和字段名,不能有歧义。行为约束则是一些兜底规则,比如"参数不确定时先追问用户""不要编造动作名"。

这里有个容易踩的坑:动作清单不要写得太长太啰嗦。我一开始把每个动作的文档全塞进系统提示,结果提示词膨胀到几千 token,模型反而抓不住重点,经常选错动作。后来改成"一句话用途 + 参数名列表 + 一个示例",准确率明显回升。

3.2 用少样本示例锚定输出格式

光靠文字描述格式,模型还是可能跑偏。最有效的手段是给两三个少样本示例(few-shot),直接把"输入-输出"的完整样子摆出来。

示例要覆盖几种典型情况:一个直接回答不调工具的、一个调单个工具的、一个调工具后根据结果再决策的。这样模型能学到"什么时候该调、什么时候不该调"以及"调用的文本长什么样"。我实测下来,加了三个示例之后,格式解析成功率从七成多提到了九成五以上。

示例的写法也有讲究。思考部分要写得像人话,不要写成机械的"我需要调用X";动作部分严格用约定的标识符;参数部分用标准 JSON。让模型模仿的样本越接近真实使用场景,它的输出就越稳。

3.3 参数校验失败时怎么让模型自我修复

即使 Prompt 写得再好,模型偶尔还是会输出解析不了的参数,比如少个引号、类型不对、字段名拼错。这时候不要直接报错退出,而是把解析错误信息作为"观察"喂回给模型,让它自己修。

我的做法是:解析器捕获异常后,生成一条类似"参数解析失败:缺少字段 city,请重新输出动作"的观察文本,拼进下一轮上下文。模型看到自己的错误,通常下一轮就能改对。这个自修复机制能挡掉大部分偶发格式问题,让 Agent 的鲁棒性上一个台阶。

注意:自修复要有次数上限。我设的是连续失败三次就终止并返回错误,避免模型陷入"改不对-再改-还不对"的死循环,白白烧 token。

3.4 防止 Prompt 被误判:几个实操细节

做这类 Agent 时,Prompt 内容本身也可能触发一些平台的输入检查,导致请求被拒。我遇到过几次提示词被判定为"可能违规"的情况,排查后发现往往是一些看起来无害但触发了规则的措辞。

规避思路有几个。一是避免在 Prompt 里堆砌大量特殊符号和转义字符,尤其是连续的反斜杠、引号嵌套,容易被规则误伤。二是少用绝对化的指令词,把"必须""绝对"这类词换成更平和的表述,语义不变但更安全。三是把长 Prompt 拆成系统提示和用户消息两部分,分散内容密度。四是对动态拼接进 Prompt 的用户输入做清洗,去掉控制字符和异常长的连续符号串。

这些细节不影响功能,但能显著降低请求被拒的概率。踩过几次坑之后,我现在写 Prompt 会刻意保持"干净",不追求花哨的格式。

4. 运行时实现:解析、执行与循环控制

4.1 解析器的健壮性设计

解析器是整个 Agent 的心脏,它必须足够健壮。我的解析器分三步走:先按分隔符切出"思考/动作/参数"三段,再对动作名做白名单校验,最后对参数做 JSON 解析和类型转换。

切分这一步最容易出问题,因为模型可能不按格式来。我的策略是宽松匹配:不要求严格的行首对齐,只要能在文本里找到"动作:"这个标记,就从它后面开始提取。参数部分则从第一个左花括号找到最后一个右花括号,中间的内容整体拿去解析。这样即使模型多写了几个字、少了个换行,也能兜住。

动作名校验用白名单,不在注册表里的动作一律拒绝,并返回"未知动作"的观察让模型重试。这一步能有效防止模型"幻觉"出一个不存在的工具。参数解析失败则走前面说的自修复流程。

4.2 动作执行层的隔离与超时

动作执行函数不能直接裸奔。我给每个动作都套了一层包装:超时控制、异常捕获、结果裁剪。

超时控制是必须的,尤其是调用外部接口的动作,万一对方卡住,整个 Agent 就挂起了。我统一设了 10 秒超时,超时就返回"动作执行超时"的观察。异常捕获保证任何动作抛错都不会让主循环崩溃,而是转成一条错误观察喂回模型。结果裁剪则是防止某个动作返回几万字的原始数据,把上下文撑爆——超过一定长度就截断或摘要。

这三层包装让 Agent 在面对不可靠的外部依赖时依然能稳定运行。我线上跑的那套,连续运行几周没出现过因为单个动作失败导致整体崩溃的情况。

4.3 上下文管理与 token 控制

多轮循环最大的敌人是上下文膨胀。每一轮的思考、动作、观察都往上下文里堆,几轮下来 token 就爆了。我的处理方式是滑动窗口 + 关键信息保留。

滑动窗口保留最近 N 轮的完整交互,更早的轮次只保留"动作 + 观察结果"的摘要,丢掉冗长的思考文本。同时,系统提示和最初的用户问题始终保留在上下文头部,不参与滑动。这样既控制了长度,又不丢失任务目标。

另外,工具返回的长文本我会在拼进上下文前先做一次压缩,比如只保留前若干字符加一句"(内容已截断)"。实测下来,这套组合能把一个几十轮的任务稳定控制在合理 token 范围内。

4.4 循环终止与结果收敛

循环什么时候停,必须由运行时说了算。我的终止条件有三个:模型输出finish动作、达到最大轮数上限、连续解析失败超限。

finish是正常出口,模型认为任务完成时输出它,参数里带上最终答案。最大轮数是个保险丝,防止模型陷入无意义的循环,我一般设 15 到 20 轮。连续失败超限则是异常出口,返回一个明确的错误信息而不是让用户干等。

这里有个经验:最大轮数不要设太小。有些任务确实需要多轮工具调用才能完成,设成 5 轮会导致任务半途而废。我踩过这个坑,后来调到 20 轮,配合上下文压缩,既够用又不至于失控。

5. 实测中暴露的问题与我的应对

5.1 模型"忘记"输出格式的几种典型场景

跑了一段时间后,我总结出模型最容易跑偏格式的几种情况。一是任务特别简单时,模型觉得"没必要走流程",直接给答案,不输出动作标记。二是上下文特别长时,模型被前面的内容带偏,格式开始松散。三是用户输入本身包含类似格式的文本时,模型可能被"带节奏"。

应对办法:在系统提示里反复强调格式要求,并在每轮的用户消息末尾追加一句轻量的格式提醒。对于简单任务,我干脆在 Prompt 里明确"即使能直接回答,也要走 finish 动作输出"。这些补丁加起来,格式稳定性提升很明显。

5.2 动作选择错误的排查链路

模型选错动作是最常见的功能性问题。排查时我一般按这个链路走:先看模型的思考段落,判断它是"理解错了任务"还是"知道要干嘛但选错了工具";再看动作清单的描述是否清晰,很多时候是描述有歧义导致模型选错;最后看少样本示例是否覆盖了这类场景。

有一次模型总是把"查询订单"和"查询物流"搞混,我检查后发现两个动作的描述都写了"查询订单相关信息",边界模糊。把描述改成"查询订单的支付状态和金额"和"查询订单的配送进度和当前位置"之后,混淆就消失了。动作描述要写到"互斥"的程度,这是血泪教训。

5.3 长任务下的上下文漂移

任务轮数一多,模型容易"忘记"最初的目标,开始跑偏。这是上下文漂移问题。我的应对是在每轮上下文里都保留最初的任务描述,并在系统提示里加一句"始终围绕用户的原始目标行动"。

另外,我会在关键节点做一次"目标复述",让模型在思考里明确当前进度和剩余步骤。这个动作看似多余,但能显著降低漂移概率。实测下来,加了目标复述之后,长任务的完成率提升了不少。

5.4 解析失败率与模型能力的关系

同一个 Prompt,换不同能力的模型,解析失败率差别很大。能力强的模型格式遵循度高,能力弱的模型经常漏字段、写错分隔符。所以解析器的健壮性要和模型能力匹配:用弱模型时,解析器要更宽松,自修复要更积极;用强模型时,可以适当收紧校验,早点暴露问题。

我现在的做法是给解析器留一个"宽松度"配置,不同模型走不同档位。这个设计让同一套 Agent 代码能适配多个模型,迁移成本很低。

6. 无 Tool Calling 方案的适用边界与取舍

6.1 什么场景下这套方案更划算

不是所有场景都适合无 Tool Calling。我的判断标准是:当你需要强控制、强可移植、强可调试时,这套方案更划算。

具体来说,如果你的 Agent 要对接多个模型供应商、要在私有环境部署、要对每一步决策做审计、要频繁增删工具,那无 Tool Calling 的优势就很明显。反过来,如果你只是快速做个 Demo、只用一个模型、工具集固定,那原生 Tool Calling 更省事,没必要折腾。

我自己的项目之所以选这条路,核心原因是可移植性。业务要求 Agent 能在不同模型间切换,原生 Tool Calling 的 schema 各家不统一,切换成本太高。用文本协议之后,换模型只需要调 Prompt,代码几乎不动。

6.2 和原生 Tool Calling 的对比

维度原生 Tool Calling无 Tool Calling
控制流归属模型厂商自己的运行时
模型可移植性差,各家 schema 不同好,只依赖文本输出
调试透明度低,结构化对象难读高,思考过程可见
格式稳定性高,厂商保证中,依赖 Prompt 质量
中间处理能力弱,难插入自定义逻辑强,每步都可干预
开发成本低,开箱即用中,需自建解析和执行层
能力扩展改 schema 注册改注册表 + Prompt

这张表是我实际用下来总结的,不是纸上谈兵。可以看到两者各有取舍,没有绝对优劣,关键看你的约束条件。

6.3 混合方案:什么时候该把 Tool Calling 加回来

实践中我发现,纯无 Tool Calling 和纯原生 Tool Calling 都不是最优解,混合往往更好。比如对于格式要求极高、参数复杂的动作,可以让模型走原生 Tool Calling 保证结构正确;对于需要灵活推理、需要中间干预的决策,走文本协议。

我的一个项目就是这么做的:核心决策循环用文本协议,保证可控;少数几个参数复杂的查询动作,用原生 Tool Calling 兜底。两者通过统一的动作注册表协调,对外表现一致。这种混合方案兼顾了稳定性和灵活性,是我目前最推荐的落地形态。

6.4 我踩过的三个印象最深的坑

第一个坑是过度依赖正则解析。一开始我用正则抠动作和参数,结果模型稍微换个措辞就匹配不上。后来改成基于标记的宽松切分,鲁棒性好了很多。正则适合校验,不适合做主解析。

第二个坑是忽略了思考段落的 token 成本。思考段落虽然不参与解析,但它占 token。长任务里思考越写越长,成本飙升。后来我在 Prompt 里限制了思考长度,要求"简洁",成本降下来了,效果没受影响。

第三个坑是没有给动作执行设超时。有个外部接口偶发卡死,导致整个 Agent 挂起,用户那边一直转圈。加了超时之后,这类问题就变成了可恢复的观察,Agent 能自己绕过去。

7. 从零搭一个最小可用的结构化 Agent

7.1 目录结构与模块划分

我把最小实现拆成四个模块:prompt.py管系统提示和示例,parser.py管解析,actions.py管动作注册表和执行,agent.py管主循环。这样拆的好处是每块职责单一,改 Prompt 不影响解析,加动作不影响循环。

目录大概长这样:

agent/ prompt.py # 系统提示、少样本示例 parser.py # 文本协议解析器 actions.py # 动作注册表 + 执行包装 agent.py # ReAct 主循环 main.py # 入口

这个结构不复杂,但足够支撑一个能跑通的通用 Agent。后面要扩展,往对应模块加东西就行。

7.2 动作注册表的最小实现

动作注册表的核心就是一个字典加一个装饰器。装饰器负责把函数注册进去,同时记录它的参数说明,供 Prompt 生成时使用。

ACTIONS = {} def register(name, desc, params): def wrapper(fn): ACTIONS[name] = {"fn": fn, "desc": desc, "params": params} return fn return wrapper @register("get_weather", "查询指定城市天气", {"city": "城市名"}) def get_weather(city): # 实际实现省略 return f"{city}今天晴,25度"

这样新增动作只要写个函数加个装饰器,Prompt 里的动作清单可以自动从ACTIONS生成,不用手动同步。这个自动化很关键,否则动作一多,Prompt 和代码就容易对不上。

7.3 主循环的骨架代码

主循环的逻辑很直白:拼上下文、调模型、解析输出、执行动作、拼观察、判断终止。

def run_agent(user_input, max_turns=20): context = build_initial_context(user_input) for turn in range(max_turns): raw = call_llm(context) parsed = parse(raw) if parsed.action == "finish": return parsed.params.get("answer", "") result = execute_action(parsed.action, parsed.params) context = append_observation(context, result) return "达到最大轮数,任务未完成"

这段骨架看着简单,但每一行背后都有前面讲的细节:parse要健壮,execute_action要有超时和异常捕获,append_observation要做上下文压缩。把这些细节填进去,就是一个能上生产的循环。

7.4 跑通第一个任务的验证清单

搭完之后,我会用几个任务验证:一个纯问答(不调工具)、一个单工具调用、一个多工具串联、一个会触发解析失败需要自修复的。四个都跑通,基本说明这套 Agent 是健康的。

验证时重点看三件事:格式解析成功率、动作选择准确率、循环能否正常终止。这三个指标达标,就可以往里面加真实业务动作了。我每次重构 Agent 都会跑一遍这个清单,能挡掉大部分低级问题。

8. 一些不写在文档里的经验

做这类 Agent 最深的体会是:模型不是不可靠,而是它的可靠性需要你用工程手段去"框住"。无 Tool Calling 的本质,就是把这份"框"从厂商手里拿回来,自己掌控。代价是要多写一些解析和校验代码,收益是换来了可移植、可调试、可扩展。

另一个体会是,Prompt 工程不是玄学,它是有反馈回路的工程活。你改一版 Prompt,跑一批测试用例,看解析成功率和动作准确率的变化,这就是可度量的优化。我现在的 Prompt 都是迭代了十几版才稳定的,没有一版是拍脑袋写出来的。

最后分享一个小技巧:把每次解析失败和动作错误的样本存下来,定期回看。这些失败样本是最宝贵的优化素材,比任何理论都管用。我攒了几百条失败样本,每次优化 Prompt 都拿出来对照,效果立竿见影。这套方法不依赖任何特定框架,换到哪个项目都能用。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/27 15:43:21

AI加速OLED光学仿真:深度学习实现千倍效率提升

1. 项目概述:AI如何重构OLED光学仿真流程在OLED显示面板的研发过程中,光学仿真一直是制约产品迭代速度的关键瓶颈。传统基于物理模型的光学仿真需要消耗大量计算资源,单次仿真往往需要数小时甚至数天时间。而最新研究表明,通过深度…

作者头像 李华
网站建设 2026/10/27 15:42:38

MCP v2.0范式迁移:从会话式调用到事件驱动流式编排

1. MCP 不是“又一个协议”,而是开发范式的一次断层式迁移MCP——Model Communication Protocol,最近半年在开发者社区里被反复提起,但绝大多数人还在用“它是不是类似 LSP 或 DAP 那种语言服务器协议”的旧框架去理解它。这恰恰是踩坑的起点…

作者头像 李华
网站建设 2026/10/27 15:42:37

SpringBoot3 WebSocket鉴权实战:握手拦截器实现登录验证

做后端的朋友大概都有过这样的经历:项目里要加一个实时消息推送,第一反应是上轮询,结果服务器压力一上来就顶不住;换成 WebSocket 之后,连接是通了,可一到鉴权环节就卡壳——HTTP 那套 Session、Token 的玩…

作者头像 李华
网站建设 2026/10/27 15:42:09

SQL并行处理MCP架构设计与性能优化实践

1. SQL执行MCP工具开发背景解析在数据库管理领域,SQL执行效率一直是DBA和开发人员关注的核心问题。MCP(Multi-Channel Processing)作为一种并行处理架构,能够显著提升批量SQL的执行性能。我在金融行业数据迁移项目中首次接触这个概…

作者头像 李华
网站建设 2026/10/27 15:41:21

顺丰同城盈利模式解析:动态定价与混合运力的技术突破

1. 即时配送行业的盈利困境与突破点 即时配送赛道近年来呈现出爆发式增长态势,但行业普遍面临"增量不增收"的盈利难题。数据显示,2022年中国即时配送订单量突破400亿单,市场规模超过2000亿元,但行业平均利润率不足3%。这…

作者头像 李华
网站建设 2026/10/27 15:39:41

WSL2搭建AI开发环境:GPU直通CUDA配置与PyTorch训练实战

1. 为什么我把 AI 开发主力环境从双系统搬到了 WSL2先说结论:如果你在 Windows 主力机上做深度学习或大模型相关开发,又不想每次训练都要重启进 Ubuntu,那么 WSL2 是目前最值得尝试的中间态方案。先交代背景。我主要做 CV 方向的模型训练和推…

作者头像 李华