news 2026/10/28 5:39:11

绕开Tool Calling:结构化通用Agent的工程化实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
绕开Tool Calling:结构化通用Agent的工程化实践

1. 为什么我要绕开 Tool Calling 做 Agent

1.1 一个被过度神化的机制

过去一年里,只要聊到 Agent 开发,几乎所有人第一反应都是 Tool Calling。模型厂商把它包装成 Agent 的标配能力,各种框架也把它当成默认路径。我一开始也是这么干的,接上工具定义、让模型自己决定调哪个函数、拿到返回值再继续推理,看起来非常优雅。

但真到了生产环境,问题就来了。Tool Calling 本质上依赖模型厂商在推理层面对特定格式的支持,不同厂商的实现细节差异极大。同一个工具定义,在 A 模型上跑得好好的,换到 B 模型就开始乱调参数,甚至干脆不调。更麻烦的是,一旦你用的是本地部署的开源模型,或者某些不支持原生 Tool Calling 的推理接口,整条链路直接断掉。

我踩过最狠的一次坑,是给一个客户做内部知识库 Agent。开发阶段用的是某家 API,Tool Calling 稳得不行。结果客户要求私有化部署,换成了本地模型,工具调用成功率从 95% 掉到 40% 都不到。那段时间我天天对着日志怀疑人生,最后发现根本不是 Prompt 写得不好,而是模型对工具调用格式的理解压根就不一致。

1.2 结构化通用 Agent 的核心思路

绕开 Tool Calling,不代表放弃工具能力。我的做法是把"调用工具"这件事,从模型的特殊能力降级成普通的文本生成任务。模型不需要知道什么是 function call,它只需要按照我规定的格式,输出一段结构化的文本,我在外面解析这段文本,执行对应的操作,再把结果塞回上下文。

这个思路听起来很朴素,但它带来的好处是实打实的。第一,模型无关性。只要模型能稳定输出 JSON 或者特定标记格式的文本,就能跑 Agent,不管它是 GPT、Claude 还是本地跑的 Qwen、Llama。第二,可控性。所有工具调用的解析、校验、执行都在我自己的代码里,出问题能定位到具体哪一步,而不是黑盒里猜。第三,调试友好。模型的每一次输出都是纯文本,我可以直接打印出来看,不用去翻那些嵌套好几层的 tool_calls 结构。

这里说的"结构化",指的是模型输出有明确的格式约束,而不是让模型自由发挥。格式约束越清晰,解析越稳定,Agent 的可靠性就越高。

1.3 这套方案适合谁

如果你正在做 Agent 相关的项目,但遇到以下任何一种情况,这套方案值得你认真看一下:用的是不支持原生 Tool Calling 的模型;需要在多个模型之间切换;对 Agent 的执行过程要求可观测、可干预;或者单纯觉得 Tool Calling 的黑盒感太强,想自己掌控全流程。

反过来,如果你只是做个 Demo,用官方 API 加 Tool Calling 五分钟就能跑通,那没必要折腾。这套方案的价值在于工程可控性,而不是开发速度。我个人的经验是,Demo 阶段怎么快怎么来,一旦要上生产、要换模型、要排查问题,结构化方案的优势就会立刻体现出来。

2. 结构化 Agent 的整体架构拆解

2.1 核心循环:ReAct 的简化与改造

ReAct 模式大家应该都不陌生,Thought、Action、Observation 三步循环。标准 ReAct 里,Action 那一步通常就是 Tool Calling。我把它改成了纯文本协议:模型在 Action 阶段输出一段固定格式的文本,比如以特定标记开头,后面跟 JSON 参数。外层代码用正则或者字符串切分把这段内容抠出来,解析成结构化数据,再路由到对应的工具函数。

整个循环的骨架大概是这样:系统 Prompt 定义好输出格式和可用工具列表,用户输入进来,模型生成一段包含思考和行动指令的文本,解析器提取行动指令,执行工具,把结果作为 Observation 拼回对话历史,再次调用模型,直到模型输出最终答案或者达到最大轮次。

这个改造的关键点在于,Prompt 里必须把格式规则写得极其明确。我一般会给出两到三个完整的输出示例,覆盖"需要调用工具"和"直接回答"两种情况。示例比规则管用,这一点在结构化输出上体现得特别明显。

2.2 为什么不用 JSON Mode

有人会问,既然要结构化,为什么不直接用 JSON Mode 或者 Structured Output 功能。我的答案是:能用当然好,但不能依赖。JSON Mode 同样是模型厂商的特性,换模型就可能失效。而且 JSON Mode 通常只保证输出是合法 JSON,不保证字段符合你的业务语义。

我的做法是双保险:Prompt 里要求模型输出特定标记包裹的 JSON,外层解析器先做格式校验,再做字段校验,任何一步失败就触发重试或者降级处理。这样即使模型偶尔抽风,整个系统也不会直接崩掉。实测下来,加了校验和重试之后,工具调用的有效率达到 98% 以上,比裸用 Tool Calling 还稳。

2.3 上下文管理的基本策略

Agent 跑多轮之后,上下文会迅速膨胀。我的策略是分层管理:系统 Prompt 和工具定义放在最前面,这部分是固定的;中间是对话历史和工具调用记录,这部分会增长;最后是当前轮次的指令。

对于中间部分,我做了滑动窗口加摘要的处理。超过一定轮次或者 token 数之后,把早期的工具调用记录压缩成一句话摘要,只保留关键结论。这样既控制了 token 消耗,又不会丢失重要信息。具体阈值我一般设在上下文窗口的 60% 左右,留出足够的空间给模型生成。

上下文管理是 Agent 工程里最容易被忽视、但影响最大的环节。我见过太多项目因为上下文爆炸导致模型开始胡言乱语,最后归咎于"模型不行",其实是工程没做好。

3. 核心细节:Prompt 设计与解析器实现

3.1 系统 Prompt 的写法

系统 Prompt 是这套方案的地基。我一般把它分成四个部分:角色定义、工具清单、输出格式规范、行为约束。

角色定义要简短,一句话说清楚这个 Agent 是干什么的。工具清单用列表形式,每个工具写清楚名称、用途、参数说明。输出格式规范是重点,我会明确告诉模型:需要调用工具时,输出以特定标记开头的内容,后面跟 JSON;不需要调用工具时,直接输出最终答案。行为约束包括最大思考轮次、遇到不确定情况怎么处理、禁止编造工具返回值等。

这里有个细节很多人会忽略:工具参数的描述要足够具体。比如一个查询天气的工具,参数 city 要说明是城市名称,最好给出示例。模型对参数的理解完全依赖你的描述,描述模糊,参数就乱填。

3.2 输出格式的约定

我常用的格式是这样的:模型需要调用工具时,输出一段以ACTION:开头的文本,后面跟一个 JSON 对象,包含tool和params两个字段。不需要调用工具时,输出以ANSWER:开头的文本,后面跟最终答案。

选择这种标记式格式而不是纯 JSON,是因为纯 JSON 容易和模型的其他输出混在一起,解析时不好切分。标记式格式有明确的边界,正则一抓就出来。而且标记本身对模型来说是个强提示,看到ACTION:就知道该输出结构化内容了。

格式示例我会在 Prompt 里给两到三个,覆盖单工具调用、多工具调用、直接回答三种情况。示例要真实,参数要具体,不要用 foo、bar 这种占位符,模型会照抄的。

3.3 解析器的健壮性设计

解析器是整个方案里最需要打磨的部分。我的解析器分三层:第一层用正则提取标记块,第二层解析 JSON,第三层校验字段。

第一层正则要写得宽松一点,允许模型在标记前后有多余的空格或者换行。第二层 JSON 解析要捕获异常,解析失败时记录原始文本,方便排查。第三层字段校验要检查必填字段是否存在、类型是否正确、工具名称是否在允许列表里。

任何一层失败,都不直接报错,而是把失败原因作为 Observation 塞回上下文,让模型重新生成。这个重试机制非常关键,它把模型的偶发错误变成了可恢复的问题。我一般设置最多重试两次,两次还不行就返回兜底答案。

import re import json ACTION_PATTERN = re.compile(r"ACTION:\s*(\{.*?\})", re.DOTALL) def parse_action(text): match = ACTION_PATTERN.search(text) if not match: return None, "未找到 ACTION 标记" raw = match.group(1) try: data = json.loads(raw) except json.JSONDecodeError as e: return None, f"JSON 解析失败: {e}" if "tool" not in data or "params" not in data: return None, "缺少 tool 或 params 字段" return data, None

这段代码是我实际项目里简化出来的,核心就是宽松提取加严格校验。注意正则用了非贪婪匹配,避免把后面的内容也吞进去。

3.4 工具注册与路由

工具本身用字典注册,键是工具名,值是一个包含描述、参数 schema、执行函数的对象。路由的时候根据解析出来的 tool 字段查字典,找不到就返回错误信息。

每个工具的执行函数我都要求做异常捕获,返回统一的格式:成功时返回结果字符串,失败时返回错误描述。这样模型拿到的 Observation 格式一致,不会因为工具报错导致整个流程乱掉。

工具描述我建议写得像给新人看的文档,说清楚这个工具能做什么、不能做什么、参数怎么填。模型理解工具的能力,完全取决于你的描述质量。

4. 完整实操:从零搭一个能跑的结构化 Agent

4.1 环境准备与依赖

Python 环境我一般用 3.10 以上,主要是类型提示和 match 语法用着舒服。依赖很少,核心就是模型调用库,其他都是标准库。如果你用的是 OpenAI 兼容接口,装个 openai 包就行;如果用本地模型,看具体推理框架的文档。

我特意保持依赖精简,就是为了让这套方案容易迁移。不引入 LangChain 这类重框架,是因为框架的抽象层会掩盖细节,出问题不好排查。自己写两百行代码,比调框架调一天强。

pip install openai

就这一个,其他用标准库。requests 都不用,模型调用库自带 HTTP 客户端。

4.2 工具函数的编写

我拿两个工具举例:一个是计算器,一个是查当前时间。计算器用 Python 的 eval 有安全风险,我用 ast 模块做安全求值,只允许数字和基本运算符。查时间直接用 datetime。

import ast import operator from datetime import datetime OPS = { ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul, ast.Div: operator.truediv, } def safe_eval(node): if isinstance(node, ast.Num): return node.n if isinstance(node, ast.BinOp): return OPS[type(node.op)](safe_eval(node.left), safe_eval(node.right)) raise ValueError("不支持的表达式") def calculator(expression): try: tree = ast.parse(expression, mode="eval") return str(safe_eval(tree.body)) except Exception as e: return f"计算失败: {e}" def get_time(): return datetime.now().strftime("%Y-%m-%d %H:%M:%S")

安全求值这块我要多说一句。很多人图省事直接用 eval,这在 Agent 场景下是灾难。模型可能生成任何东西,eval 等于把执行权交给了模型。用 ast 白名单方式,只放行你明确支持的运算,这是底线。

4.3 主循环的实现

主循环的逻辑很直白:拼上下文、调模型、解析输出、执行工具、拼回上下文、判断是否结束。

def run_agent(user_input, max_turns=5): messages = [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": user_input}, ] for turn in range(max_turns): response = call_model(messages) messages.append({"role": "assistant", "content": response}) action, err = parse_action(response) if action is None: if "ANSWER:" in response: return response.split("ANSWER:", 1)[1].strip() messages.append({"role": "user", "content": f"格式错误: {err},请重新输出"}) continue result = execute_tool(action["tool"], action["params"]) messages.append({"role": "user", "content": f"OBSERVATION: {result}"}) return "达到最大轮次,未能完成任务"

这里有几个设计决策值得说。第一,工具执行结果用OBSERVATION:标记,和 ACTION 对应,让模型清楚这是工具返回的内容。第二,格式错误时不是直接失败,而是把错误信息塞回去让模型重试。第三,最大轮次限制是必须的,防止模型陷入死循环。

4.4 一次完整的执行记录

我拿"帮我算一下 123 乘以 456,再告诉我现在几点"这个输入跑一遍。

第一轮,模型输出ACTION: {"tool": "calculator", "params": {"expression": "123 * 456"}}。解析成功,执行计算器,得到 56088。Observation 塞回去。

第二轮,模型输出ACTION: {"tool": "get_time", "params": {}}。解析成功,执行查时间,得到当前时间。Observation 塞回去。

第三轮,模型输出ANSWER: 123 乘以 456 等于 56088,现在是 2024-01-15 14:30:22。检测到 ANSWER 标记,返回最终答案。

整个过程三轮,两次工具调用,全部走文本协议。你可以把模型换成任何一个能遵循格式的模型,流程完全一样。

4.5 参数校验的补充

实际项目里,工具参数往往需要更严格的校验。比如计算器的 expression 参数,我会检查长度不超过 200 字符,只包含数字、运算符和空格。查时间的工具没有参数,但我会检查 params 是不是空字典。

这些校验看起来琐碎,但能挡住大量模型抽风的情况。我的原则是:凡是模型能生成的,都要校验。宁可多写几行校验代码,也不要让脏数据流进工具函数。

5. 踩坑记录与问题排查

5.1 模型不遵循格式怎么办

这是最常见的问题。模型有时候会忘记标记,直接输出 JSON,或者在 ACTION 后面跟一段解释再跟 JSON。我的处理方式是:Prompt 里把格式规则放在最显眼的位置,用分隔线隔开;给足示例;解析器尽量宽松。

如果还是不行,我会在系统 Prompt 里加一句"任何情况下都必须严格遵循输出格式,不要添加额外解释"。这句话对大部分模型有效。再不行就换模型,有些小模型确实对格式指令不敏感,硬调不如换。

5.2 JSON 解析失败的排查

JSON 解析失败通常有几个原因:模型输出了单引号、尾随逗号、注释、或者中文标点。我的解析器在标准 json.loads 失败后,会尝试做一轮清洗:把单引号换成双引号、去掉尾随逗号、去掉注释。清洗后再解析,成功率能提升不少。

如果清洗后还是失败,就把原始文本记到日志里。我一般会攒一批失败样本,分析规律,然后针对性地改 Prompt。这个过程很枯燥,但效果立竿见影。

5.3 工具调用死循环

模型有时候会反复调用同一个工具,尤其是工具返回的结果不符合它预期的时候。我的处理是在 Observation 里加上轮次提示,比如"这是第 2 次调用该工具",让模型意识到自己在重复。同时设置单个工具的最大调用次数,超过就强制中断。

还有一种情况是模型在两个工具之间来回跳,这种一般是 Prompt 里的工具描述有歧义,模型分不清该用哪个。解决办法是把工具描述写得更互斥,明确每个工具的适用边界。

5.4 常见问题速查表

问题现象可能原因排查方向解决手段
模型不输出 ACTION 标记Prompt 格式说明不清晰检查系统 Prompt 的格式部分增加示例,强化格式指令
JSON 解析失败模型输出非法 JSON打印原始输出增加清洗逻辑,重试
工具名找不到模型编造工具名检查工具列表描述校验工具名,返回错误让模型重试
参数类型错误参数描述模糊检查参数 schema补充参数说明和示例
死循环工具结果不符合预期查看调用历史加轮次提示和调用次数限制
上下文爆炸历史记录过长统计 token 数滑动窗口加摘要压缩

这张表是我从实际项目里总结出来的,基本覆盖了 90% 以上的问题。遇到新问题先对照这张表,大部分情况能快速定位。

5.5 几个容易被忽视的细节

第一个细节是模型输出的稳定性。同一个输入,模型可能这次输出 ACTION,下次直接输出答案。这不是 bug,是模型的随机性。我的做法是在 Prompt 里明确"如果需要使用工具才能回答,必须先输出 ACTION",把决策逻辑写死。

第二个细节是 Observation 的格式。我坚持用固定前缀,是因为模型对格式化的上下文更敏感。你如果直接把工具返回值裸塞进去,模型有时候会分不清这是用户说的还是工具返回的。

第三个细节是错误信息的措辞。给模型看的错误信息要具体、可操作,比如"params 字段缺少 expression 参数",而不是"参数错误"。模型根据你的错误信息来修正,信息越具体,修正越准。

6. 性能优化与扩展方向

6.1 Token 消耗的控制

结构化方案的 token 消耗主要在三块:系统 Prompt、对话历史、模型输出。系统 Prompt 是固定的,优化空间在于精简工具描述,把不常用的工具说明压缩。对话历史用滑动窗口加摘要,前面说过了。模型输出这块,可以通过限制最大生成长度来控制,一般设 500 token 足够。

我实测过,同样的任务,结构化方案比 Tool Calling 的 token 消耗高 10% 到 20%,主要多在格式标记和示例上。但这个代价换来的是模型无关性和可控性,我认为值得。如果你的场景对成本极度敏感,可以精简示例,只保留最核心的一个。

6.2 并发与流式输出

Agent 主循环本身是串行的,因为每一步依赖上一步的结果。但工具执行可以并发,如果模型一次输出了多个 ACTION,可以并行执行再一起塞回去。不过大部分模型一次只输出一个 ACTION,这个优化的收益有限。

流式输出对 Agent 来说比较尴尬,因为你需要完整输出才能解析 ACTION。我的做法是流式输出给用户看,但内部等完整输出再解析。用户体验和解析稳定性兼顾。

6.3 多 Agent 协作的扩展

这套结构化协议天然适合多 Agent 协作。每个 Agent 有自己的工具集和系统 Prompt,Agent 之间通过结构化消息通信。一个 Agent 的输出可以作为另一个 Agent 的输入,格式统一,解析逻辑复用。

我做过一个简单的多 Agent 实验:一个规划 Agent 负责拆解任务,一个执行 Agent 负责调工具,一个校验 Agent 负责检查结果。三个 Agent 用同一套结构化协议,串起来跑,效果比单 Agent 好不少。当然复杂度也上去了,是否值得看具体场景。

6.4 安全边界的加固

Agent 安全是个大话题,这里只说和结构化方案相关的部分。因为所有工具调用都经过我的解析器和路由,我可以在这一层做细粒度的权限控制。比如某些工具只允许特定用户调用,某些参数需要额外校验,某些操作需要二次确认。

这种集中式的控制点,比 Tool Calling 分散在各处的调用要安全得多。我可以在解析器里加审计日志,记录每一次工具调用的完整信息,出问题能追溯。这是结构化方案在安全上的一个隐性优势。

工具执行函数的异常捕获一定要做,而且要把异常信息转成模型能理解的描述。直接抛异常会让整个 Agent 崩掉,转成 Observation 则能让模型自己想办法绕过。

7. 我在这套方案上的一些个人体会

这套结构化 Agent 方案我用了大半年,从最初的原型到现在的生产系统,中间改了很多版。最大的体会是:Agent 的可靠性不取决于模型多强,而取决于工程做得多细。格式约束、解析校验、重试机制、上下文管理,这些看起来不起眼的东西,才是决定 Agent 能不能上生产的关键。

Tool Calling 不是不能用,而是不该被当成唯一路径。把它当成一种可选能力,而不是架构的基础假设,你的 Agent 会灵活很多。我现在做新项目,默认就是结构化方案起步,需要的时候再考虑要不要接 Tool Calling 做优化。

最后分享一个小技巧:把每次 Agent 执行的完整对话历史存下来,定期分析。你会发现很多问题是有规律的,比如某类输入特别容易触发格式错误,某个工具的描述容易引起歧义。这些规律是优化 Prompt 和工具设计的最好依据。我靠这个习惯,把工具调用成功率从 85% 提到了 98% 以上,比任何框架升级都管用。

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

混合检索技术与重排序模型实践指南

1. 相似度计算与混合检索技术解析相似度计算是信息检索和推荐系统的核心基础技术。在实际工程中,我们通常需要处理多种类型的相似度计算场景:文本相似度:计算两段文本的语义相似程度向量相似度:比较高维向量空间中的距离结构相似度…

作者头像 李华
网站建设 2026/10/28 5:38:54

GitHub热榜技术脉搏图谱:TypeScript+Python+Shell铁三角解析

1. 项目本质解构:这不是“榜单”,而是一份实时技术脉搏图谱很多人点开“GitHub 热榜项目:日榜(2026-10-03)”这个标题,第一反应是——又一个刷屏的排行榜截图。但作为连续跟踪 GitHub Trending 超七年、手动…

作者头像 李华
网站建设 2026/10/28 5:38:10

二叉树前序遍历测试用例设计与工程实践

1. 二叉树前序遍历测试用例设计精要当我在代码评审会上第一次看到这个测试用例编号144时,立刻意识到这是二叉树算法测试中一个经典而关键的案例。前序遍历作为二叉树最基本的遍历方式之一,看似简单却暗藏玄机。在实际工程中,约23%的二叉树相关…

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

从JD到胜任力图谱:AI岗位能力建模实战指南

1. 从一份岗位说明书说起:为什么传统JD在AI团队里越来越不够用我做了十多年技术管理,带过算法团队、数据团队,也帮不少公司做过技术岗位的招聘和人才盘点。这几年最直观的感受就是:传统岗位说明书(JD)在AI相…

作者头像 李华
网站建设 2026/10/28 5:37:26

考研英语词汇与长难句突破:100句真题回译法

简介:这份PDF资料面向备战考研英语的考生及英语学习者,针对背单词难、长难句读不懂、阅读速度慢等常见痛点,提供一套以句子为单位的词汇与语法突破方案。全书精选考研真题中的100个典型句子,逐句讲解核心词、同义词、反义词、同根…

作者头像 李华
网站建设 2026/10/28 5:36:19

impeccable用法详解:从词源到搭配,避免高级词汇翻车

如果你问一个英语学习者,过去一年里你最想“显摆”的形容词是什么,十有八九会有人提到 impeccable。这个词听起来高级,拼写有腔调,说出口仿佛全身都在发光。但前几天我在英语角遇到一位学员,很自豪地说出“My English …

作者头像 李华