Grok function calling 与工具体系怎么用
数据截至 2026-08,价格与限额以各官网为准。本文只讲计费与接入机制,不列具体价格数字。
一句话讲完:xAI 官方文档把工具明确分成两类——内置工具(Built-in Tools,如 Web Search、X Search、Code Interpreter、Image Generation、Collections Search)在 xAI 服务器上自动执行完再把结果喂给模型;function calling 是你自己定义的函数,模型只负责发出调用请求,执行这一步会「暂停并把控制权交还给你的应用」。真正让人栽跟头的从来不是怎么写一个 tool 定义,而是这三件事:一,parameters 的根必须是 object(或者每个分支都是 object 的 oneOf/anyOf),否则会被 400 拒绝并在错误里点名是哪个工具;二,两类工具混用时 max_turns 只约束单次请求内的服务端轮次,客户端工具调用等于一个检查点,续接的那次请求会重新起算;三,判断一个返回项到底要不要你本地执行,xAI SDK 用 get_tool_call_type,Responses API 看 response.output[].type 是不是 function_call,两套判据不能混着用。
先分清:谁执行、在哪执行
xAI 官方的 Tools Overview 页把这件事说得很直白:内置工具由 xAI 托管、在服务端执行,你只提供工具配置,API 负责执行并返回结果;function calling 则是「你定义的自定义函数,模型可以请求调用」,什么时候发生什么完全由你控制。
这个分界线决定了你代码的形状。用内置工具时,你的请求里只需要写上 {"type": "web_search"} 这样的一个类型声明,剩下的模型自己调、自己看结果、自己接着想;而用 function calling 时,请求发出去以后一定会回到你手上一次,你得把函数跑完、把结果塞回去、再发一次请求。
官方给的整体流程是五步:分析查询、决定下一步做什么、执行工具(内置)或返回工具调用请求(function calling)、处理结果并继续、直到信息足够返回最终响应,并在适用时带上引用。注意第三步里那个「或」——同一次请求里,两条路径是并存的。
定义一个 function 工具:三个必填字段
官方的 Tool Schema Reference 表里,name、description、parameters 三个字段全是必填。name 是唯一标识符,官方还在同一行注明了单次请求可传的工具数量上限(具体数值以官方文档当前版本为准);description 用来「帮助模型判断什么时候该用这个工具」——这句话值得抄在便签上,很多人把 description 写成一句话敷衍,然后抱怨模型该调的时候不调;parameters 是定义函数入参的 JSON Schema。
Python 侧官方给了两种写法。一种是直接把 dict 塞进 xai_sdk.chat.tool(...),另一种是用 Pydantic 定义模型再 .model_json_schema() 生成 schema,官方称之为「类型安全的参数 schema」。后者的好处是你的函数签名和给模型的声明是同一份来源,改一处两处都跟着变,不会出现「schema 里写了 unit 参数,函数根本没这个形参」这种低级错。
schema 根节点的硬性约束
这是整页文档里最容易被忽略、又最容易直接把请求打挂的一条。官方原文的要求是:parameters schema 的根必须是一个对象("type": "object"),其他类型要嵌在 properties 里面;根节点写成 anyOf 或 oneOf 也可以,但前提是每一个分支本身都是 object——这样你就能定义一个接受多种对象变体的工具,比如一个通知工具,一支叫 email 带 address 字段,一支叫 sms 带 phone 字段。
反过来,官方用一个 WARNING 块写明了失败情形:如果 parameters 的根既不是对象、也不是对象的联合(例如是标量、是数组、或者是含有非对象分支的 anyOf/oneOf),就无法被编译成工具调用的语法约束(tool-call grammar),会返回 400 错误,并且错误信息里会点名是哪个工具。
这个错误信息设计得挺体贴——你传了一堆工具,它会告诉你是哪一个坏了。所以遇到 400 别急着怀疑鉴权,先把报错里那个工具名捞出来看它的 schema 根。
调用循环:从 tool_call 到结果回填
官方把流程拆成五步:定义工具(名称、描述、参数 JSON Schema)→ 把工具放进请求 → 模型在需要外部数据时返回一个 tool_call → 你在本地执行函数并把结果返回 → 模型带着你的结果继续。
具体到代码,两条 SDK 路径的字段名是不一样的,这是迁移时最容易翻车的地方:
- xAI SDK:从
response.tool_calls里取,每一项的tool_call.function.name是函数名,tool_call.function.arguments是 JSON 字符串形式的参数,需要自己json.loads。回填时先chat.append(response)把这一轮的助手消息接上,再对每个调用chat.append(tool_result(json.dumps(result))),然后重新chat.sample()。 - OpenAI SDK 兼容路径:遍历
response.output,判断item.type == "function_call",取item.name和item.arguments。回填时构造一个{"type": "function_call_output", "call_id": item.call_id, "output": ...}的输入项,配合previous_response_id=response.id再发一次client.responses.create。
注意 call_id 这个字段——在 Responses API 这条路上,结果和调用是靠它对上号的,不是靠顺序。官方的示例里还顺手演示了一个防御写法:如果模型给出的函数名不在你的 tools_map 里,就把 {"error": "Unknown function: ..."} 作为输出返回去,而不是让代码抛 KeyError。这是个值得照抄的习惯。
Vercel AI SDK 那条路径则把定义、执行、请求响应循环整个封装掉了,工具的 execute 回调直接写在工具定义里,循环由 stopWhen: stepCountIs(...) 控制。方便,但也意味着你少了一层观察点。
tool_choice 和并行调用这两个开关
tool_choice 官方列了四种取值,属于结构性枚举,可以照抄(以官方文档为准):"auto" 是默认,模型自己决定用不用工具;"required" 要求模型至少调用一个工具;"none" 直接关掉工具调用;传 {"type": "function", "function": {"name": "..."}} 则是强制指定某一个工具。
"required" 和强制指定这两档在做数据抽取型任务时特别有用——你不希望模型「聊两句就完了」,而是必须走结构化出口。"none" 则常用在同一套 prompt 想复用、但这一轮不想让它动外部系统的场合。
并行 function calling 官方说明是默认开启的:模型可以在单次响应里请求多个工具调用,你需要把它们全部处理完再继续。想关掉,在请求里传 parallel_tool_calls: false。
这里有个实现细节值得留意:既然默认可以一次回多个,你的处理代码就不能写成「取第一个 tool_call」。官方示例里全是 for tool_call in response.tool_calls: 这样的循环,不是随便写写的。
混用内置工具时的两个反直觉点
官方的 Function Calling 页明确说了 function calling 可以和内置的 agentic 工具并存——同一个 tools 数组里既放 web_search()、x_search(),也放你自己的 tool(name="save_to_database", ...)。模型可以先去搜,再调你的函数把结果存下来。
第一个反直觉点在 max_turns。官方 Advanced Usage 页专门开了一节讲:max_turns 只限制单次请求内的助手/服务端工具调用轮次。当模型决定调用客户端工具时,agent 执行会暂停并把控制权交还给你的应用,当前请求就此结束;你执行完、把结果接上去、发起的是一次新的后续请求,而这次请求会以全新的 max_turns 计数重新开始。官方原话的意思是,客户端工具调用相当于「检查点」,会把轮次计数器重置。所以你以为设了上限就锁死了总开销,实际上并没有——真正的总量取决于往返了几轮。
顺带一提,max_turns 本身也不是「最多调几次工具」。官方特意强调它限制的是 agentic 循环的助手轮次,而单个轮次里模型可以并行调用多个工具。没传这个参数时,服务端会应用一个全局默认上限;达到上限后 agent 停止继续调工具,基于已收集到的信息生成最终回答(默认行为以官方文档当前版本为准)。
第二个反直觉点是类型判定。你拿到一堆返回项,怎么知道哪些要自己跑?两套 API 两套判据:xAI SDK 用 xai_sdk.tools.get_tool_call_type,返回值是 "client_side_tool"、"web_search_tool"、"x_search_tool"、"code_execution_tool"、"collections_search_tool"、"mcp_tool" 之一,只有第一个需要本地执行;Responses API 则看 response.output[].type,"function_call" 是客户端的,"web_search_call"、"x_search_call"、"code_interpreter_call"、"file_search_call"、"mcp_call" 都由 xAI 服务端处理。
官方示例里还补了一句很实在的注释:服务端的输出项暴露的是类型特有的字段(比如 action),而不是像客户端 function_call 那样有 name 和 arguments。所以别拿一套取值逻辑去啃两类项,取字段之前先按上面那两张表判类型,判完再决定读哪些字段。
流式模式下的两个例外
Function Calling 页开头就挂了个 WARNING:在流式模式下,函数调用是在单个 chunk 里整体返回的,不是跨 chunk 逐段流式输出的。也就是说你不用自己拼参数片段,拿到就是完整的一份。
另一个例外在 Tool Usage Details 页:流式 agentic 请求可以通过 chunk 上的 tool_calls 属性实时观察模型做出的每一个工具调用决策,这一节同时给了个 Note,说服务端工具的调用输出不在 API 响应里返回,agent 只在内部用这些输出来形成最终答案。
但读到这句千万别停,否则很容易得出「服务端工具搜回来的东西根本拿不到」这个结论——而它是被同一份文档另一节直接推翻的。Streaming & Sync 页专门有一节 Accessing Tool Outputs,把话说全了:服务端工具调用的输出默认不返回,原因是它们可能体积很大;但你可以显式 opt-in 把它们要回来。两条路径各有一张对照表,取值属于结构性枚举,具体以官方文档当前版本为准:
- xAI SDK:在
include字段里传对应取值——web_search对web_search_call_output,x_search对x_search_call_output,code_execution对code_execution_call_output,collections_search对collections_search_call_output,attachment_search对attachment_search_call_output,mcp对mcp_call_output。官方示例的写法是建 chat 时带上include=["code_execution_call_output"],之后照常 stream 或 sample。 - Responses API:这一套的工具名和 include 取值都换了形状——
web_search对web_search_call.action.sources;code_execution在 Responses API 里叫code_interpreter,对code_interpreter_call.outputs;collections_search叫file_search,对file_search_call.results;mcp那一行官方标的是在 Responses API 里始终返回,也就是不用你去 opt-in。
所以准确的说法是:默认只看得见「它去搜了什么」,看不见「搜回来什么」;想看见,就把对应的输出取值加进 include。这个默认设计本身也提示了一件事——这些输出确实可能很大,开之前先想清楚是长期打开还是只在排查时临时打开,因为它们会实打实地进到你的响应体里。要做结果溯源,除了 opt-in 拿原始输出,还有 citations 这条更轻的路,官方的流式和同步示例最后都在打印 response.citations。
官方推荐 agentic 工具调用用流式模式,理由是能实时观察工具调用、在可能较长的请求过程中拿到反馈、以及看到推理 token 计数。同步模式(chat.sample())当然也支持,适合逻辑简单、不需要中间观测的场景。
用量口径:两个字段不是一回事
Tool Usage Details 页把两个容易混淆的指标掰开了讲,这段对做成本归因的人很重要:
response.tool_calls返回的是 agentic 过程中所有被尝试的工具调用,每项包含id、function.name、function.arguments,包括失败的那些。response.server_side_tool_usage返回的是成功执行的工具及其调用次数的映射,官方原文写明它「决定你的计费」。
两者出现差异的原因官方也列了:工具执行失败(比如去浏览一个不存在的网页、抓一条已删除的 X 帖子)、参数无效无法处理、以及网络或服务临时故障。官方的计费说明是——只有成功执行的工具调用会计费,失败的尝试不计费。
用量类目和函数名之间还隔着一层映射。比如 SERVER_SIDE_TOOL_WEB_SEARCH 这个类目底下会出现 web_search、web_search_with_snippets、browse_page、open_page、open_page_with_find 等多个具体函数名;X 搜索类目底下则是 x_user_search、x_keyword_search、x_semantic_search、x_thread_fetch。做归因时如果你只按函数名分组,同一类工具会被拆成好几行对不上账。
token 侧也有 agentic 请求特有的读法:completion_tokens 只代表最终文本输出,官方提醒这个数往往比你预期的小得多,因为中间的推理和工具编排都在内部完成;prompt_tokens 是整个 agentic 过程中所有推理请求的累计输入,每次请求都带着到那一刻为止的完整对话历史,会随着 agent 推进而增长。官方同时指出,正因为步与步之间大部分提示不变,agentic 请求能显著受益于提示缓存,cached_prompt_text_tokens 就是看有多少提示 token 是从缓存里来的。工具类请求的计费由 token 用量和工具调用次数两部分构成,具体单价以官方定价页为准,这里不复述数字。想把缓存这块吃透,可以看Grok 缓存怎么影响计费那篇。
多轮里怎么把 agentic 状态带下去
工具调用一旦跨了多轮,「上一轮它搜到啥、推理到哪一步」要不要留,就成了必须做的选择。官方给了两条路:
一是把对话历史存在 xAI 服务端。首次 agentic 请求时加 store_messages=True,服务端会存下完整对话历史,包括模型的推理、服务端工具调用及对应响应;续接时传 previous_response_id=response.id。官方补了一句很关键的说明:续接的那次对话不需要和首次用同样的工具、模型参数或其他配置,它依然会被完整地注入上一轮的 agentic 状态。
二是走加密内容。xAI SDK 侧是 use_encrypted_content=True,OpenAI SDK 兼容路径是 include=["reasoning.encrypted_content"],然后自己把 response.output 累加进 input_list 里带下去。这条路适合不想把对话历史留在对方服务端的场景。
选哪条其实是数据驻留策略问题,不是技术优劣问题。接入前先把这个问题和合规同事对齐,比写完再改省事得多。
最后:三个最容易栽的坑
第一,把 400 归因错。schema 根节点不是 object 会被直接拒,错误信息里点了工具名,别浪费时间去查 key。写工具定义时养成习惯:根一律 "type": "object",变体用全 object 分支的 oneOf。
第二,以为 max_turns 锁住了总量。客户端工具是检查点,会重置计数。要控总开销,得在你自己的 while 循环里也加一个往返次数上限,光靠请求参数不够。第三,只处理第一个 tool_call。并行 function calling 默认开着,官方在这一节给出的要求是把这一轮返回的调用全部处理完再继续(Process all of them before continuing),所以处理代码必须写成遍历所有调用项的循环,不能取到一个就往下走。不想要这个行为,就在请求里传 parallel_tool_calls: false 把它关掉,而不是在处理侧偷懒。
还没跑通第一个请求的,先看Grok API 怎么接入把鉴权和 base_url 理顺;准备上生产的,把工具调用次数也纳入监控口径,做法参考API 成本监控怎么做;密钥别硬编码在工具执行逻辑里,API Key 安全管理那篇讲了几种常见的泄露路径。