阶跃星辰 JSON mode 与工具调用怎么写:字段、回传与踩坑
数据截至 2026-08,价格与限额以各官网为准。本文只讲计费与接入机制,不列具体价格数字。
阶跃星辰把「让模型吐结构化结果」和「让模型调外部函数」拆成了两组参数:前者靠 response_format,后者靠 tools。两组参数在 Chat Completions 和 Responses 两套接口里的写法并不一样——Chat Completions 用嵌套的 function 对象加顶层 response_format,Responses 则把工具字段拍平、把输出格式挪进了 text.format。真正会把人绊倒的不是这些字段名,而是三件事:arguments 回来的是字符串不是对象、工具结果必须靠 tool_call_id 原路回传、以及输出预算不够时响应会以「没有最终消息」的形态返回。这篇按官方文档逐个字段过一遍,并标出文档里前后写法不一致的地方。
先分清你在用哪套接口
两套接口都在同一个域下,官方示例里客户端初始化用的 base URL 是 https://api.stepfun.com/v1,因为是 OpenAI 兼容形态,直接用 OpenAI 的 SDK 指过去就行。但 Chat Completions 和 Responses 的参数结构是两码事,抄错一套字段名,报的错往往指向参数非法,而不是提示你「用错接口了」。
Chat Completions 这边:工具列表放在顶层 tools,每个成员是 {"type": "function", "function": {...}} 的两层结构,type 当前只有 function 一个取值;输出格式放在顶层 response_format。
Responses 这边:tools 里的成员是扁平的,type、name、description、parameters、strict 全在同一层,不再套一层 function 对象;官方文档写明当前仅支持 function 类型工具;输出格式则挪到了 text.format 下面。
同一件事两种写法,最省事的做法是把工具定义单独抽成一份数据结构,再写两个适配函数分别拍成两种形状,而不是在业务代码里到处 if。核心思路是把「协议形状」隔离在最外面一层,业务代码只认自己那份定义。
JSON mode:官方给的是三步,不是一个开关
阶跃官方指南把 JSON Mode 的用法明确写成三件事,顺序不能颠倒:
- 在 System Prompt 里放上你期望的 JSON 结构和说明,官方推荐用 JSON Schema 的结构描述来帮助模型理解;
- 请求时把
response_format设成{ "type": "json_object" }; - 解析模型返回的结果,并验证是否符合预期,符合预期后再对接进业务系统。
第一步最容易被跳过。很多人以为打开 json_object 就万事大吉,但官方明确说的是「在 System Prompt 中放置你预期大模型给出的输出的 JSON 的结构和说明」——开关只保证输出是可解析的 JSON,字段长什么样得你自己在提示词里讲清楚。官方给的示例做法是在提示词里直接贴一段 JSON Schema,声明每个属性的类型、含义,并用 required 数组标出模型必须返回的字段。
response_format 的 type 在请求参数文档里列了三个取值:text、json_object、json_schema,默认是 text(默认行为以官方文档当前版本为准)。第三个取值是比 JSON Mode 更强的一档:当 type 为 json_schema 时,json_schema 对象必填,里面 name 是 schema 的标识名称、schema 是遵循 JSON Schema 规范的定义对象,两者都是必填;另有一个可选的 strict 布尔字段,官方说明是开启后模型输出将严格遵循所定义的 schema。
所以这里其实有一条清晰的升级路径:先用 json_object 加提示词里的 Schema 描述跑通,字段稳定之后再迁到 json_schema 把约束交给服务端。前者靠模型理解,后者靠格式约束,可靠性不在一个量级上,但后者要求你把 schema 写死,业务字段还在变的阶段反而会拖慢迭代。
拿到结果先看 finish_reason
官方在 JSON Mode 的注意事项里专门提醒:使用 JSON Mode 时需要检查返回结果的 finish_reason 是否为 stop;如果是 length,说明模型受到最大输出 token 限制无法完整返回内容,所返回的 Message 可能无法被正常解析。
这一条值钱在于它解释了一类特别难查的线上故障:JSON 解析报错,但重放同样的请求有时又好了。原因不是模型「时好时坏」,而是输出长度顶到了上限,JSON 被从中间截断,末尾少了花括号。请求参数文档里 finish_reason 一共列了三个取值:stop(正常结束)、length(达到 max_tokens 上限)、tool_calls(模型发起工具调用)。解析前先判一下这个字段,比在 try/except 里吞掉异常有用得多。
Responses 接口那边有个对应的坑,官方用 Note 单独标了出来:max_output_tokens 会同时限制推理过程和最终输出;在使用较高推理强度、JSON Schema、视频等复杂输入时,建议预留更大的输出预算;预算不足时响应可能返回 status="incomplete",output 里可能只有 reasoning 项而没有最终的 message 项。也就是说预算被推理过程吃光了,你拿到的是一个结构完整但没有正文的响应——如果代码直接去取 message,这里会空指针。
工具定义:字段约束比想象中严
tools 里每个函数的三个字段,官方都给了具体约束,不是随便写:
name:工具调用文档给出的规则是使用英文字母、数字、下划线与连字符,遵循正则^[a-zA-Z_][a-zA-Z0-9_-]{0,63}$。注意首字符不能是数字,也不能是连字符。官方还补了一句,使用语义化的英文名更易被模型识别。description:支持中英文,用于告诉模型这个函数的用途与适用场景,便于模型判断何时调用。官方直说「描述越清晰,模型命中越准确」。parameters:根节点type必须为object;properties里每个字段按 JSON Schema 规范描述其type和description;若参数必填,要在required数组中列出。
官方在注意事项里还给了两条经验:构造工具时要把函数的 description 描述清楚以提高命中率;入参的 description 也需要备注清楚,并且建议说明入参是中文还是英文,以便模型生成有效的参数。示例里那个查天气的函数就是这么干的——参数说明直接写了「城市名称(必须为汉字)」。这个细节挺实在:模型不会自己猜你的下游接口吃中文还是拼音。
另外有一条容易被忽略的成本相关约束:官方说函数数量建议控制在合理规模,过多的函数定义会消耗较多 prompt token,并可能降低模型的命中准确率。工具定义是每轮都要随请求发上去的,挂了几十个工具就等于给每次对话都加了一段固定开销,这笔账要算进你的成本监控里。
tool_choice 在两套文档里的口径不一样
这一点值得单独说:Chat Completions 的请求参数章节里没有列出 tool_choice 这个字段,但官方指南的 Python 示例代码里确实传了 tool_choice="auto",注释写的是「自动选择是否调用外部函数」。Responses 接口的参数文档则明确列了 tool_choice,并写明当前仅支持字符串 "auto",含义是由模型自行决定是否调用工具。
换句话说,如果你指望像别的平台那样用 tool_choice 强制模型必须调某个指定函数,官方文档里没有找到相关说明。真要控制调用时机,能用的手段还是把 description 写清楚,或者干脆分两次请求、由你自己的代码来决定这一轮要不要挂工具。
模型返回工具调用之后,怎么把结果接回去
模型决定调用工具时,message.content 会是空串,实际内容在 tool_calls 数组里。每个成员包含三部分:id 是函数执行 ID,由模型生成、在 context 中保持唯一;type 默认为 function;function 里是 name 和 arguments。
arguments 是字符串,不是对象。 官方文档对它的说明是「函数执行的参数,一般为 json 结构化对象」,但返回示例里它的值形如 "{\"formula\": \"(80 + 20) / 5\"}",带着转义引号——官方 Python 示例里也是先 json.loads 再取字段的。直接把它当 dict 用是新手最常见的一类崩溃。
回传这一步官方写得很清楚:当模型返回 tool_calls 后,开发者在本地执行对应函数,再通过 role: "tool" 的消息把结果回传至下一轮请求,这条消息要携带 tool_call_id,而这个 ID 由 assistant 在上一轮对话中返回。消息体本身的 content 就是函数执行得到的内容。模型据此生成最终回答。
所以完整链路是四段:你发请求带 tools → 模型回 tool_calls → 你本地执行拿到结果 → 你把结果作为 role: "tool" 消息连同之前的上下文一起再发一次。少了最后一次请求,用户是拿不到自然语言回答的,只会拿到一堆函数参数。这套双轮机制在各家的实现里是相通的,不熟的话可以先看函数调用的通用原理。
判断有没有命中工具,官方指南示例用的写法是先看 message.content.strip() 是否为空串,再去取 tool_calls[0]。而请求参数文档里 finish_reason 有 tool_calls 这个取值。两处放在一起看,我更倾向于用 finish_reason 做分支、用 tool_calls 是否存在做兜底——判空串这种写法在模型既说了话又调了工具的情况下会漏判。
官方内置工具:不用你自己实现的那两个
工具调用文档里列了阶跃官方支持的工具,配置一下就能用,不需要你自己实现函数体:一个是互联网搜索,调用搜索引擎获取互联网上的最新信息;一个是知识库搜索,把文本上传到知识库中并完成对知识库内容的搜索,官方说法是帮助大模型消除幻觉。
互联网搜索的写法和自定义函数不同:type 固定为 web_search,并支持通过 function.description 描述什么情况下需要搜索,用来指引模型判断是否要调用。官方说明搜索完成后,结果会以上下文的方式插入到对话中,再交给模型推理。返回结果里 tool_calls.function.results 包含的是搜索引擎返回的信息,可以基于它渲染引用来源的 UI。
计费上要单独留意:官方写明互联网搜索工具按实际调用的次数计费,具体计费见官方定价说明页。这是一笔独立于 token 的开销,如果你把搜索工具挂在一个高频接口上,账单结构会和纯对话完全不同。
还有一处通道限制别踩:step-router-v1 只能走 Step Plan 通道,官方给的差异表里写明该通道下 tools 中的 web_search 不支持,使用会返回 unsupported_content_type;同一通道下 messages 里的图像和文档输入也不支持,返回同样的错误标识;model 字段只接受该路由模型名,填别的会返回 HTTP 400 加 request_params_invalid。也就是说「哪些工具能用」不只取决于模型,还取决于你走的是哪条通道。至于哪些模型支持工具调用,官方在工具调用页面列了推荐档、仅 Step Plan 通道可用的路由模型,以及其他兼容项三类,具体名单以官方文档为准——这份名单会随模型迭代变化,写死在代码注释里迟早过期。
工具调用和缓存是有关系的
一个容易被漏掉的联动:官方 Prompt 缓存文档在「什么样的内容会进入缓存」里明确写了,对话信息中的工具调用信息及其调用结果也会参与到缓存当中。
这意味着上面那套「两次请求」的链路天然对缓存友好——第二次请求带着完整的历史消息,前缀和第一次高度重合。但反过来,如果你每轮都动态重排工具列表的顺序、或者在系统提示词里塞入时间戳这类每次都变的内容,前缀一变缓存就白搭。官方给的最佳实践就是尽可能保持 Prompt 前缀的稳定。
判断有没有命中,看响应的 usage 字段。这里有个文档本身的小出入值得提一句:缓存文档正文写的是「如果 response.usage 存在 cached_token 字段」,而同一页的返回示例和工具调用页的返回示例里,字段名都是 cached_tokens(带 s)。写解析代码时两个键都兼容一下更稳妥。缓存部分的 token 按低于标准价的比例计费,具体比例见官方定价页;缓存的淘汰采用最近最少使用策略,官方说明在系统请求高峰期不使用的缓存会更容易被逐出。想把这块算清楚,可以对照缓存计费的通用机制来估。
最容易栽的三个坑
第一个是把 arguments 当对象用。它是字符串,且是模型生成的,即便开了严格模式也建议解析后再校验一遍字段是否齐全——官方 JSON Mode 的第三步写的就是「解析并验证是否符合预期」,这句话对工具参数同样适用。
第二个是不看 finish_reason 就解析。JSON 被截断和响应正常结束,在代码里长得一模一样,只有这个字段能区分。Responses 接口还多一种形态:status="incomplete" 且 output 里只有推理项。
第三个是把两套接口的字段混着写。Chat Completions 的 tools 是套了一层 function 的,Responses 的是平的;输出格式一个在 response_format、一个在 text.format。这类错误通常以参数非法的形式返回,和限流、鉴权类错误的处理路径完全不同,别一股脑丢进通用的重试逻辑里——参数错重试多少次都还是错。
下一步建议这么做:先用 json_object 加提示词里的 Schema 把链路跑通,确认 finish_reason 的分支处理到位;再挂一个最简单的自定义工具,把 role: "tool" 回传那一环走通;最后才考虑上 json_schema 的严格模式和内置搜索工具。顺序反过来,出了问题你会分不清是格式约束的事还是回传链路的事。