使用 OpenAI Function Calling 原生工具
作者:程序员马丁
Ragent AI —— 从 0 到 1 纯手工打造企业级 Agentic RAG,拒绝 Demo 玩具!AI 时代,助你拿个offer。
上一篇咱们把系统提示词从最小版升级到了结构化的完整版:角色与边界、格式锚定、Few-shot 示例、注意事项四个模块一上,格式稳定性从 60% 拉到了 95%+。大脑现在基本能稳定地按 Thought: / Action: / Action Input: 格式输出了。
但你有没有注意到一个事实——从第 05 篇到现在,咱们一直在跟文本解析较劲。第 05 篇写了 parseAction() 按行扫描提取工具名和参数,第 06 篇为了支持多行 JSON 入参把解析器改成了多行收集版,第 07 篇又花了大半篇幅打磨提示词来锚定输出格式。三篇文章、三轮加固,本质上都是在给同一个问题打补丁:大模型的输出是自由文本,而你需要从里面精确提取结构化数据。
这一篇,咱们换个思路——不再从文本里提取结构化数据,而是让模型直接返回结构化数据。这就是 Function Calling。
升级到 Function Calling 之后,TinyAgent 的循环模式和 Spring AI、LangChain4j 等主流框架完全一致——这就是现代工具调用 Agent 的工业主路径。它保留了 ReAct 的 Act → Observe → Loop 工程骨架,但模型的推理过程(Thought)从显式的文本标签变成了隐式的内部决策。换句话说,大脑还在想,只是不一定把想法写出来了。如果你的场景需要把推理过程显式拿回来做审计或调试,可以在 Function Calling 框架内加上可观测的推理通道。
本项目中具体代码已上传 GitHub TinyAgent,大家 Clone 项目后,将代码 分支切换到 1.3.x,默认主分支是最新代码。运行前复制
.env.example为.env,把自己的 API Key 填进去,默认阿里云百炼平台;.env已加入.gitignore,切分支时不会丢。
文本解析的三个硬伤
在上 Function Calling 之前,先把文本解析方案的天花板说清楚——不是做得不好,而是这条路本身有顶。
1. parseAction 永远是脆弱的
parseAction() 的核心逻辑是按行扫描,找到 Action: 开头的行提取工具名,找到 Action Input: 开头的行提取参数。这种做法有一个根本性的假设:大模型的输出严格遵循预期格式。
但大模型的输出是概率性的。即使提示词写得再严格、Few-shot 示例给得再完整,你也无法保证模型永远按格式输出。冒号从英文变成中文(Action:)、标签前多了个空格( Action:)、换了个大小写(action:)——任何一个微小偏差都可能导致解析失败。
你当然可以用正则表达式做模糊匹配、加更多的容错逻辑,但这条路越走越复杂,最终变成一个越补越大的筛子。
2. stop 序列是个 hack
第 05 篇里,咱们在 API 请求里加了 stop: ["Observation:"],防止大脑自己编造工具返回结果。这个做法管用,但本质上是一个 hack——你在用 API 的停止机制来弥补文本协议的缺陷。
如果模型输出的文本里恰好包含 Observation: 这个词(比如在 Thought 里引用了前一轮的结果),stop 序列会提前截断,导致模型的思考被意外打断。虽然概率不高,但一旦发生就很难排查。
3. 提示词被格式约束占据了大量空间
看看上一篇升级后的系统提示词——900 字里,至少有 400 字是格式约束和 Few-shot 示例。这些内容的唯一目的就是教大脑怎么输出 Thought: / Action: / Action Input: 格式。每次 API 调用都要发送这 400 字的格式教程,每一圈循环都要消耗这部分 Token。
如果有一种方式能让模型直接返回结构化的工具调用,这 400 字的格式约束、Few-shot 示例、stop 序列,全部可以删掉。
快速回顾 Function Calling
如果你跟着 RAG 系列一路走过来,Function Calling 应该不陌生——那篇文章里已经详细讲过它的原理和协议细节。这里只快速回顾核心概念,重点放在它怎么用在 Agent 循环里。
1. 核心机制
Function Calling 是 OpenAI 在 Chat Completions API 里提供的一个能力:你在请求里传入一组工具定义(tools 参数),模型分析用户问题后,如果认为需要调用工具,会在响应里返回一个结构化的 tool_calls 字段,包含函数名和参数——不是文本,是 JSON。
你的代码发送:messages + tools(工具定义)
↓
模型返回:tool_calls: [{id, function: {name, arguments}}]
↓
你的代码执行工具,把结果用 tool 角色消息发回
↓
模型看到结果,继续推理或给出最终答复
关键点:模型不执行工具,它只是告诉你应该调这个工具,参数是这些。执行工具、拿回结果、再发给模型——这些都是你的代码做的。
2. 跟文本 ReAct 的本质区别
文本 ReAct 是一种提示词协议——你在提示词里定义 Thought: / Action: / Action Input: 格式,模型照着格式输出文本,你的代码从文本里提取信息。
Function Calling 是一种 API 协议——你通过 API 参数传入工具定义,模型通过 API 响应的结构化字段返回工具调用。
打个比方:文本 ReAct 就像你跟同事约定需要我做什么就写在便利贴上,格式是工具名加参数,同事写字潦草的时候你就看不懂。Function Calling 就像公司上了一套工单系统,同事在系统里选工具、填参数、提交,你收到的永远是格式标准的工单——不存在看不懂的情况。

从文本 ReAct 到 Function Calling:到底改了什么
升级到 Function Calling,核心代码逻辑没变——还是 Agent 循环,还是推理-行动-观测的三元组。变的是三个东西:工具怎么告诉模型、模型怎么告诉你要调什么、工具结果怎么传回模型。