1. 为什么 Claude Code 工具调用里要强制 JSON 输出在 Claude Code 课程的工具使用章节里前两节我们让模型调用计算器、查询天气模型返回的是tool_use块里面input字段天然就是结构化对象。但很多同学在练习时会遇到一个尴尬场景直接让模型“返回 JSON”结果拿到一段带解释文字、带 Markdown 代码围栏、甚至字段名拼错的字符串还得写正则去抠。这就是本节要解决的问题——利用工具定义tool schema来强制模型输出合法 JSON。核心思路其实很朴素模型一旦决定“调用工具”它就必须按照你给的input_schema来填参数。我们并不真的去执行这个工具函数只把tool_use.input取出来当结果用。这相当于给模型套了一个格式模具它以为自己在调工具实际上我们在做结构化抽取。这个技巧适合谁适合正在做 Claude Code 课程练习的同学、需要从非结构化文本里抽实体/做情感分析/做分类的开发者以及想把模型输出直接喂给下游程序数据库、前端表格、自动化流程的工程同学。关键词就是 Claude code、工具使用、JSON 结构化输出。我试过在同一个 prompt 里既要求“返回 JSON”又给工具定义结果模型有时会走纯文本路线格式飘忽。后来统一改成“只允许用工具”配合tool_choice参数稳定性立刻上来了。下面我会把工具定义 JSON 片段、强制参数配置、以及用 curl 验证返回结构的完整动作都写清楚你可以直接复制到课程练习里跑。在进入配置之前先说明一个工程上的现实问题课程示例里模型调用是直连的但真实练习中你往往要同时接多个工具、多个模型Key 管理会很乱。所以本节会结合 TaoToken 的统一 Key/API 通道来做让 Claude Code 的工具调用请求走一个稳定的入口避免在多个 Key 之间来回切换。2. TaoToken 统一 Key 通道的前置准备与接入文档要把上面的强制 JSON 技巧跑通第一步是让请求能稳定发出去。Claude Code 课程里的示例代码用的是 Anthropic SDK默认读环境变量里的 Key。如果你同时练多个模型、多个工具每个都配一套 Key很容易在切换时把 A 的 Key 填到 B 的 base_url 上报 401 还找不到原因。TaoToken 在这里的作用就是提供统一的 Key 和 API 通道把模型对话、coding plan、控制台、API Keys 管理收敛到一个入口。你需要先拿到两样东西一个可用的 API Key以及统一的 Base URL。Base URL 用https://taotoken.net/api注意这个地址不带任何查询参数直接作为 SDK 的base_url或 curl 的请求前缀。Key 则在控制台的 API Keys 页面创建创建后复制保存后面所有配置都复用它。具体入口我列一下方便你按需跳转模型对话入口在 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 可以用来先在网页上验证模型是否正常响应API Keys 管理在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 创建和吊销 Key 都在这里接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 里面有各语言 SDK 的 base_url 填法如果你后面要做长期编码或 Agent可以看 coding planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。环境变量配置建议这样写Linux/macOS 用 exportWindows PowerShell 用$env:export ANTHROPIC_BASE_URLhttps://taotoken.net/api export ANTHROPIC_API_KEY你的Key注意这里变量名用的是 Anthropic SDK 认的ANTHROPIC_BASE_URL和ANTHROPIC_API_KEY这样课程里的Anthropic()客户端不用改代码就能读到。如果你用的是 OpenAI 兼容风格的调用变量名换成OPENAI_BASE_URL和OPENAI_API_KEY值里的 base_url 同样填https://taotoken.net/api。这里有个容易踩的坑base_url 末尾不要自己加/v1或/messagesSDK 会自己拼路径。我见过有同学写成https://taotoken.net/api/v1结果请求打到/api/v1/v1/messages直接 404。统一用https://taotoken.net/api就好。配好之后先别急着写工具定义用一条最简单的请求确认通道是通的。这一步能帮你把“Key 问题”和“工具 schema 问题”分开排查后面出错时心里有底。确认通了再进入下一节的工具定义和强制 JSON 配置。3. 可复制的工具定义 JSON 与强制输出配置这一节是全文的核心给你可以直接复制的工具定义片段和强制参数配置。我们以情感分析为例工具名print_sentiment_scoresinput_schema用标准 JSON Schema 描述三个必填的数值字段。{ name: print_sentiment_scores, description: Prints the sentiment scores of a given text., input_schema: { type: object, properties: { positive_score: { type: number, description: The positive sentiment score, ranging from 0.0 to 1.0. }, negative_score: { type: number, description: The negative sentiment score, ranging from 0.0 to 1.0. }, neutral_score: { type: number, description: The neutral sentiment score, ranging from 0.0 to 1.0. } }, required: [positive_score, negative_score, neutral_score] } }关键点在于required数组它保证模型必须填全三个字段不会漏。description写得越明确模型填值的语义越准比如这里限定 0.0 到 1.0模型就不会给你返回 0 到 100 的分数。接下来是强制参数配置。光靠 prompt 里写“只使用这个工具”有时会失效正确做法是用tool_choice参数锁死{ tool_choice: { type: tool, name: print_sentiment_scores } }这个配置告诉模型你必须通过调用print_sentiment_scores来响应不允许走纯文本。把它和上面的 tools 数组一起放进请求体。完整的请求体结构以 Anthropic Messages API 为例长这样{ model: claude-3-sonnet-20240229, max_tokens: 4096, tools: [ 上面那个工具定义 ], tool_choice: { type: tool, name: print_sentiment_scores }, messages: [ { role: user, content: textIm a HUGE hater of pickles./text Only use the print_sentiment_scores tool. } ] }如果你用 Python SDK代码是这样from anthropic import Anthropic import json client Anthropic() tools [ /* 上面的工具定义 */ ] response client.messages.create( modelclaude-3-sonnet-20240229, max_tokens4096, toolstools, tool_choice{type: tool, name: print_sentiment_scores}, messages[{role: user, content: textIm a HUGE hater of pickles./text Only use the print_sentiment_scores tool.}] ) for content in response.content: if content.type tool_use and content.name print_sentiment_scores: print(json.dumps(content.input, indent2)) break这里content.input就是模型填好的结构化对象直接json.dumps就是合法 JSON不需要任何正则清洗。这就是“强制 JSON”的全部秘密。再给一个实体抽取的工具定义字段是数组嵌套对象用来演示复杂结构{ name: print_entities, description: Prints extract named entities., input_schema: { type: object, properties: { entities: { type: array, items: { type: object, properties: { name: {type: string, description: The extracted entity name.}, type: {type: string, description: The entity type (e.g., PERSON, ORGANIZATION, LOCATION).}, context: {type: string, description: The context in which the entity appears in the text.} }, required: [name, type, context] } } }, required: [entities] } }注意items里也写了required这样数组里每个对象都保证有 name/type/context 三个字段下游解析时不用做空值判断。这套 schema 写法在 Claude Code 课程练习里可以直接复用换成翻译、分类、摘要任务只改字段名和 description 即可。4. 用 curl 验证返回结构是否合法配置写好了怎么确认返回的真是合法 JSON最直接的办法是用 curl 发一条请求把响应存下来再用jq校验结构。这一步在课程练习里特别重要因为 SDK 有时会把错误吞掉curl 能看到原始 HTTP 状态和响应体。先发请求把响应写到文件curl -s https://taotoken.net/api/v1/messages \ -H x-api-key: $ANTHROPIC_API_KEY \ -H anthropic-version: 2023-06-01 \ -H content-type: application/json \ -d { model: claude-3-sonnet-20240229, max_tokens: 4096, tools: [{ name: print_sentiment_scores, description: Prints the sentiment scores of a given text., input_schema: { type: object, properties: { positive_score: {type: number}, negative_score: {type: number}, neutral_score: {type: number} }, required: [positive_score, negative_score, neutral_score] } }], tool_choice: {type: tool, name: print_sentiment_scores}, messages: [{role: user, content: textI hate pickles./text Only use the print_sentiment_scores tool.}] } resp.json拿到resp.json后先看stop_reason是不是tool_use这代表模型确实走了工具调用jq .stop_reason resp.json预期输出tool_use。然后提取tool_use块里的input字段校验它是不是合法 JSON 且字段齐全jq .content[] | select(.typetool_use) | .input resp.json预期输出类似{ positive_score: 0.0, negative_score: 0.791, neutral_score: 0.209 }再用jq做一次字段存在性断言确保三个字段都在且是数字jq -e .content[] | select(.typetool_use) | .input | has(positive_score) and has(negative_score) and has(neutral_score) resp.json返回true就说明结构合法。如果返回false或报错说明模型没按 schema 填需要检查tool_choice是否生效、required是否写全。实测下来加上tool_choice后stop_reason稳定是tool_useinput字段直接就是干净的对象。你可以把这段 curl 校验写进 CI每次改 schema 后跑一遍防止字段名写错导致下游解析失败。对于实体抽取那种嵌套数组校验命令改成jq -e .content[] | select(.typetool_use) | .input.entities | length 0 resp.json确认数组非空且每个元素都有 name/type/context就说明复杂结构也稳了。5. 本篇常见报错排查401、local proxy failed、reading choices、OAuth练习过程中最容易卡住的不是 schema 本身而是请求发不出去或响应解析不了。下面按真实报错逐条排查。401 Unauthorized最常见。先确认ANTHROPIC_API_KEY环境变量在当前终端里真的生效用echo $ANTHROPIC_API_KEY看有没有值。如果值对但还报 401检查 base_url 是不是写成了带/v1的地址导致请求路径重复。还有一种情况是 Key 复制时带了空格或换行重新从 API Keys 页面复制一次。如果用的是 Claude Code 客户端检查~/.claude/settings.json里的配置是否和终端环境变量冲突。local proxy failed / connection refused这类报错通常是本地网络层的问题不是 Key 的问题。先确认ANTHROPIC_BASE_URL填的是https://taotoken.net/api没有多余端口。如果你本地跑着什么转发工具先关掉再试。用curl -v https://taotoken.net/api/v1/messages看 TCP 连接是否建立如果卡在连接阶段说明是网络出口问题换网络环境重试。reading choices of undefined这个报错一般出现在用 OpenAI 兼容 SDK 调 Anthropic 风格接口时响应结构对不上。Anthropic 的响应是content数组OpenAI 是choices数组。如果你用 OpenAI SDKbase_url 要填https://taotoken.net/api但请求路径和响应解析要按对应风格来。混用会导致 SDK 去读不存在的choices字段。解决办法是统一 SDK 和接口风格别一边用 Anthropic SDK 一边按 OpenAI 的字段名解析。OAuth / authentication_error如果你在 Claude Code 客户端里看到 OAuth 相关报错通常是客户端走了交互式登录流程而你想用 API Key 直连。检查客户端配置里是否强制走了 OAuth改成 API Key 模式把 Key 填到对应字段。如果同时出现auth.json相关提示检查~/.claude/auth.json或项目下的配置文件确保里面没有残留的旧 token 覆盖了环境变量。排查顺序建议先 curl 确认通道通再看stop_reason最后校验input结构。把这三步分开401 和 schema 问题就不会混在一起。每次改完配置用第 4 节的 jq 命令跑一遍比肉眼检查靠谱得多。6. 把强制 JSON 接入你的 Claude Code 工作流到这里工具定义、强制参数、curl 校验、报错排查都齐了。最后说下怎么把它变成日常可用的工作流。你可以把情感分析、实体抽取、翻译这几个工具定义存成一个tools.json在 Python 里json.load进来复用改任务时只换tool_choice的 name 和 prompt 里的文本。对于需要长期跑编码或 Agent 任务的场景建议把 Key 和 base_url 统一走 TaoToken 的 coding plan 通道这样多个工具、多个模型共用一套凭证切换时不用改代码。模型对话入口可以用来快速验证某个 schema 是否被模型正确理解接入文档里有各语言 SDK 的完整示例API Keys 页面负责日常的 Key 轮换。如果你在课程练习里要交作业把 curl 校验那段一起附上评审一眼就能看出你的输出是结构化且可验证的。这套方法不依赖特定模型版本换模型时只要 schema 不变tool_choice依然生效下游解析代码一行都不用动。