
400 这个状态码写过 Web 接口的人都不陌生它基本等于“你请求体的参数不对”。可一旦把它放到终端编码助手这个场景里事情就变得有点拧巴本地终端里跑的那个交互式助手说的是某一种接口方言你对接到的那台国产大模型 V4说的是另一种方言。两边各自都“没毛病”但拼在一起就是过不去。我前阵子帮人排查这个问题来回折腾了大半天从怀疑密钥、怀疑网络、怀疑版本最后发现纯粹是请求体的结构对不上——这类问题特别消耗耐心因为报错信息通常只给你一句“invalid request”连具体字段名都不给。这篇内容就是把这半天的排查过程完整摊开讲。核心关键词是400 错误、请求体结构不匹配、接口协议转换、模型名与参数越界、tools schema 兼容性。它适合三类人看正在把终端编码助手接到别的模型上、结果一直卡在 400 的人自己写网关做协议转换、被各种奇奇怪怪报错折磨的人以及单纯想搞清楚“为什么换个模型就得改一堆东西”的开发者。文中不会给你一个万能配置就完事而是把每一类 400 背后的字段级原因拆开讲让你下次能自己定位而不是靠猜。为了行文方便下面把终端里那个交互式编码助手统称为CLI 助手把对端接的国产大模型 V4 统称为V4。CLI 助手的各种环境变量名各家实现不完全一样我用CLI_BASE_URL、CLI_API_KEY、CLI_MODEL这种通用写法代替你自己对照手头那份文档替换即可。1. 先把 400 的锅分清楚报错链路上到底谁在拒绝很多人一看到 400 就去翻密钥其实这个方向从第一步就偏了。密钥错了是 401余额不足是 402 或 429权限不够是 403模型服务崩了是 500 或 502。400 的语义非常明确服务端收到了你的请求也认出了你是谁但你请求体的内容它读不懂或者不接受。换句话说认证这一层你已经过了问题在报文本身。1.1 一次调用要穿过几道关卡CLI 助手发起一次请求中间至少要过三道关每一道都可能吐 400但吐出 400 的“口吻”不太一样熟悉了其实能分辨出来。第一道是你本地。CLI 助手在组装请求体的时候会塞进去一堆东西系统提示词、工具定义、历史消息、缓存标记、思考块配置。这些字段是按它自己对接口协议的理解生成的。你在配置里指定了 base URL 指向某个兼容层它就把这一坨原封不动发过去。第二道是中间的转换层或网关。如果对端只提供 OpenAI 风格的接口而 CLI 助手说的是另一套方言中间就必须有个东西做翻译。这个翻译层是有损的——它不可能把两边所有字段都一一映射遇到自己不认识的字段处理方式通常是直接丢掉或者原样透传。原样透传是最危险的因为对端不认识这个字段直接给你一个 400。第三道才是 V4 本体。它拿到请求体做参数校验模型名对不对、max_tokens是否越界、工具 schema 是不是合法 JSON Schema、消息序列是不是符合角色交替规则。任何一条不合规返回的就是那句干巴巴的 400。所以你在终端看到的那个 400可能是三道关里任何一道发出的。很多人吃亏就吃在盯着第三道的文档改配置结果问题出在第二道的转换逻辑上。1.2 为什么 401 好查、400 难查401 好查因为原因基本唯一密钥没配对、过期了、或者前缀少了。400 难查是因为它的可能原因有十几种而且服务端出于安全考虑通常不会告诉你具体是哪个字段挂了。你只会看到类似这样的输出API Error: 400 {error:{message:Invalid request body,type:invalid_request_error}}这句话的信息量几乎为零。剩下全靠你自己二分定位。我一般的做法是先把 CLI 助手那层剥掉用 curl 或脚本直接打对端接口用最小请求体确认“服务本身能通”。通了再一层层往上加字段加到哪一步开始 400问题就锁定了。这个思路听起来笨但比盲改配置快得多因为它是可控的二分不是每次改完等十几秒看结果。提示排查期间把 CLI 助手的流式输出关掉如果有这个选项。流式模式下报错信息经常被截断在半个 JSON 里你看到的{error:{message:Inv后面就没了反而增加误判。1.3 先建立一张“责任田”地图在动手改任何东西之前我建议先画一张表把每个字段归属哪一方写清楚。这不是形式主义而是能帮你判断“这个字段该不该出现在请求里”。字段CLI 助手会发对端 V4 是否认识典型处理方式model会发且写死某个名字只认自己的模型名必须改写max_tokens必填认识但有上限超限即 400system顶层字符串或块数组OpenAI 风格只认 messages 里的 system必须搬迁tools[].input_schema一套结构只认function.parameters必须重命名映射tool_use/tool_result块消息内容里的块只认tool_calls和 roletool必须拆解重排cache_control会带大概率不认识建议直接删thinking可能带视版本而定先删了再试metadata.user_id可能带不认识删这张表做完你会发现问题集中在“结构性字段”而不是“值”。值错了往往报错很明确比如模型名不存在结构性字段错了才是不吭声的 400。2. 协议层的错位两套接口方言硬碰硬真正让我卡住的地方是两套接口在消息模型上的根本差异。这不是字段名换个写法就能解决的它牵扯到“工具调用”这个概念怎么表达。CLI 助手重度依赖工具调用它每一次读文件、执行命令都是一次 tool 往返。工具调用格式对不上400 就躲不掉。2.1 请求体长得根本不像我先把两边的最小请求体摆出来对着看你自己感受一下差异有多大。CLI 助手这一侧请求体大致是这种形状{ model: some-model-name, max_tokens: 8192, system: [ {type: text, text: 你是一个编码助手……, cache_control: {type: ephemeral}} ], messages: [ {role: user, content: [{type: text, text: 读一下 config.json}]}, {role: assistant, content: [ {type: text, text: 好的}, {type: tool_use, id: toolu_01, name: read_file, input: {path: config.json}} ]}, {role: user, content: [ {type: tool_result, tool_use_id: toolu_01, content: {\a\:1}} ]} ], tools: [ {name: read_file, description: 读取文件, input_schema: {type: object, properties: {path: {type: string}}}} ] }对端 V4 那一侧如果是 OpenAI 风格期望的是{ model: v4-chat, messages: [ {role: system, content: 你是一个编码助手……}, {role: user, content: 读一下 config.json}, {role: assistant, content: 好的, tool_calls: [ {id: call_01, type: function, function: {name: read_file, arguments: {\path\:\config.json\}}} ]}, {role: tool, tool_call_id: call_01, content: {\a\:1}} ], tools: [ {type: function, function: {name: read_file, description: 读取文件, parameters: {type: object, properties: {path: {type: string}}}}} ] }看到区别没有同样的语义结构上至少有五处不一样system 的位置、tools 的嵌套层级、工具调用的表达方式内容块 vs 独立字段、工具结果的承载方式user 消息里的块 vs 独立的 tool 角色、以及工具 id 的对齐关系。任何一处没转对对端要么报 400要么返回一堆莫名其妙的结果。2.2 转换层最容易漏掉的三个细节我自己写转换层的时候前两版都是“能跑但时不时 400”。后来复盘发现漏的是这三个细节都属于那种文档里不会强调、但实际必踩的。第一个细节is_error标记。CLI 助手在工具执行失败时会在tool_result块里带一个is_error: true。这个字段在 OpenAI 风格里没有对应物。如果原样透传对端不认识可能直接 400。正确做法是把它吞掉同时把错误信息写进 content 文本里比如加个前缀“执行失败”让模型从文本上判断而不是靠一个它不理解的结构化字段。第二个细节content既可能是字符串也可能是数组。CLI 助手发的tool_result里content有时是纯字符串有时是[{type:text,text:...}]这样的块数组。转换层如果只处理了字符串分支遇到数组分支就会把整块塞进消息里对端解析失败直接 400。这个分支一定要写成无论哪种形态最后都摊平成字符串。第三个细节连续同角色消息的合并。一次工具往返结束后CLI 助手可能会连着发两条 user 角色消息一条放 tool_result一条放新的用户输入。OpenAI 风格对消息序列的容忍度视实现而定有些严格实现要求 user 和 assistant 交替连续两条 user 直接判为非法。稳妥的转换策略是在把tool_result转成 roletool 之后如果后面紧跟一条 user 文本消息就让它们各自独立成条不要人为合并也不要干预顺序。提示转换层写完第一版别急着接上 CLI 助手跑。先拿一份真实的请求体存成 JSON 文件可以在转换层入口打日志捞然后写个脚本反复喂给转换函数断言输出结构。这比在终端里点来点去快十倍。2.3 用二分法定位到具体字段字段一多靠猜就没意义了。我常用的手法是“从最小可用体开始逐步加字段”。具体操作先准备一个绝对能通的最简请求只带模型名和一条用户消息确认返回 200。然后逐组加字段每组加完打一次看哪里开始崩。被加回去的顺序建议是这样从概率最高的开始messages里的普通文本消息几乎不会出问题用来验证基础连通system消息验证搬运逻辑单个工具定义验证 schema 映射一次带 tool_calls 的 assistant 消息验证工具调用块转换一条带 tool_call_id 的 tool 消息验证 id 对齐历史消息全套验证长序列不会被判非法这套顺序的好处是每一步的失败都能直接指向一类转换逻辑而不是笼统的“挂了”。3. 六类高频 400特征、成因、改法把排查范围收窄之后我发现真正高频的就那么六类。下面逐条拆每条都给出典型的报错特征和具体的改法。3.1 模型名不存在最常见的“假 400”按道理模型名不对该返回 404但很多兼容层图省事统一返回 400 加一句“model not found”。这类报错其实最好认因为它通常会在 message 里带上你请求的模型名。常见的写错方式有三种一是直接用 CLI 助手默认那份模型名压根没改二是改了一半大小写或者分隔符不一致有的服务用连字符有的用下划线有的带版本后缀三是加了-latest之类的别名后缀但对端只认完整名。改法很直接把CLI_MODEL显式设置成对端文档里列出的那个字符串一个字符都不要改不要自己脑补后缀不要大小写混写。我吃过一次亏把v4-chat写成V4-Chat折腾了二十分钟才发现。还有一个坑有些服务对“普通对话模型”和“推理模型”是两个不同的名字推理模型可能在参数约束上更严格比如不接受温度调节、要求思考预算。如果你只是想让 CLI 助手跑通基本功能先接普通对话模型别一上来就上推理版本。3.2 max_tokens 越界参数校验最先撞的墙CLI 助手默认的max_tokens通常设得比较大因为它要生成代码、理解长上下文动辄就是几千上万。但对端的上限可能远低于这个数。一旦超出返回的就是 400而且报错信息经常只写“invalid parameter”不说是哪个。我遇到过最无语的情况是对端上限 8192CLI 助手默认 32768每次请求都是秒拒。改法就是把CLI_MAX_TOKENS显式降下来比如降到 4096 或 8192先跑通再说。这里有个容易被忽略的点max_tokens的语义在两边并不完全一致。有的服务把它理解为“输出 token 上限”有的把它理解为“输入 输出总上限”。如果是后者而你送进去的上下文本身就有十几 K那即使你设的值没超过模型上限加起来也可能超。这种情况下光调max_tokens没用得先剪上下文。注意不要为了绕过报错把max_tokens设成 1 或者很小的值。有些服务对下限也有校验太小反而报“参数超出合法范围”。合理区间一般是 1 到模型上限之间实际用 2048 到 8192 比较稳妥。3.3 tools schema 不合规最难缠的一类工具定义这块是最容易出 400 的地方因为 JSON Schema 本身有非常多的写法而不同实现对“支持哪些子集”的宽容度差异极大。我踩过的一个典型某个工具的parameters里写了additionalProperties: false。这在前端 JSON Schema 校验里完全合法但对端拒绝理由是“不支持的 schema 关键字”。删掉就好了。类似的还有$ref引用、oneOf/anyOf组合、patternProperties、以及带正则的pattern。稳妥策略是只用最朴素的类型object、string、number、boolean、array加 properties 和 required其余一律不用。第二类问题更隐蔽参数描述里带了非法字符或者超长。有的实现会对 description 做长度截断甚至校验超过几百字符就报错。我一般把每个工具的 description 控制在 200 字以内参数级的 description 控制在 50 字以内能说明白就行。第三类是工具数量过多。CLI 助手自带的工具集可能有十几个全塞进去有的服务会对总长度或者总数做限制。如果确认是这个问题先只保留最基础的两三个工具读文件、写文件、执行命令验证连通通了再逐步放开。排查工具 schema 我有个笨办法很有效写个小脚本把tools数组里的每个工具单独抽出来逐个构造只有这一个工具的请求看是哪个具体的工具触发了 400。定位到具体工具之后再逐个删字段删到不报错为止。import json, itertools tools json.load(open(tools.json)) for t in tools: body { model: v4-chat, max_tokens: 64, messages: [{role: user, content: hi}], tools: [{ type: function, function: { name: t[name], description: t.get(description, )[:200], parameters: t[input_schema], }, }], } # 用你常用的 http 客户端打一次打印状态码和响应体 print(t[name], probe(body))这个脚本跑一遍哪个工具出问题一目了然比在终端里试快得多。3.4 system 与 content 的结构不被接受system字段的搬迁是转换层的基本功但也容易出岔子。CLI 助手有时发的是字符串有时发的是块数组带cache_control。转成 OpenAI 风格时要把它变成messages里第一条role: system的消息content是纯字符串。如果块数组里带cache_control切记把这个字段剥掉。有的实现对未知字段零容忍直接 400有的虽然忽略但如果cache_control的位置不对比如挂在了一个它认为不该挂的块上也会报错。content这一侧还有个坑空 content。CLI 助手在某些边缘情况下会发出content: 或者content: []的消息本地看没毛病但对端要求 content 非空直接 400。转换层里加一句判断如果摊平之后是空字符串就填一个占位文本比如“无内容”或者干脆把这条消息丢掉。3.5 不支持的采样参数与思考块CLI 助手默认会带temperature、top_p之类的采样参数值通常在合理范围内。但有两种情况会翻车一是推理类模型不接受采样参数。有的服务对推理模型明确要求temperature必须为 1 或者干脆不许传传了就 400。二是取值范围不同比如有的服务要求 temperature 在 0 到 1 之间CLI 助手默认 1.0 刚好卡在边界上某些实现会判为越界。处理办法转换层里做一次收敛把temperature夹到[0, 1]区间内top_p同理。如果对端是推理模型干脆整个字段删掉让它走默认值。思考块reasoning 相关内容是另一个雷区。CLI 助手如果支持“扩展思考”可能会在请求里带一个预算字段。这个字段对端不认识原样透传就是 400。排查阶段一律先删掉所有思考相关字段等基础链路跑通、确认稳定了再按对端文档决定要不要开。3.6 上下文超长被算作参数错误这一条容易被误判成“网络问题”或者“服务不稳定”因为它的表现是前几轮对话都正常聊到某个长度之后突然开始每次都 400。原因其实很直白CLI 助手会维护一段对话历史每一轮都全量发过去。历史越来越长总 token 数超过对端上限之后服务端校验失败返回 400而不是更贴切的 context_length_exceeded。判断方法看看是不是“新开一个会话就正常老会话必挂”。如果是基本就锁定了。改法有两条路。短期的在 CLI 助手里执行清空上下文的命令大部分这类助手都有/clear之类的操作把历史扔掉。长期的把对端的上下文上限配到 CLI 助手的配置里让它自己知道什么时候该压缩历史。有的助手支持配一个“上下文窗口大小”的参数把这个值设成对端真实上限的八成左右留点余量给输出。4. 完整落地从零搭一个能用的转换层光讲原因不够下面把我实际用的那套东西完整写出来。假设场景是CLI 助手说 A 方言对端 V4 只说 B 方言中间需要一层转换。我用的是一段跑在本地的轻量服务语言选的 Node.js因为它处理 JSON 改写最省事。4.1 环境变量与配置文件怎么落第一步是把 CLI 助手指向本地这一层而不是直接指向对端。变量大致是这三组# 让 CLI 助手把请求打到本地转换层 export CLI_BASE_URLhttp://127.0.0.1:8787 export CLI_API_KEYlocal-placeholder export CLI_MODELv4-chat export CLI_MAX_TOKENS4096 # 转换层自己去访问真正的对端 export UPSTREAM_BASE_URLhttps://你的对端地址/v1 export UPSTREAM_API_KEY对端的密钥 export UPSTREAM_MODELv4-chat几个细节值得说明。CLI_API_KEY这里填一个占位值就行因为认证是在转换层到对端那一段做的。填真密钥也不是不行但没必要让它多走一跳。CLI_MODEL和UPSTREAM_MODEL最好保持一致。虽然转换层会强制改写模型名但让两边看起来一样排查的时候少一层心智负担。CLI_MAX_TOKENS我设的 4096比对端上限低不少。这是故意的留出余量给系统提示词和工具定义避免“看起来没超、实际超了”的情况。注意如果你把CLI_MAX_TOKENS设得比转换层里的兜底值还大一定要在转换层里做一次Math.min收敛不能指望上游一定宽容。4.2 请求体改写一个够用的中间件下面这段是我实际在用的核心逻辑做了简化但保留了关键分支。它做四件事搬 system、改 tools、拆工具调用块、删不认识的字段。const http require(http); const UPSTREAM process.env.UPSTREAM_BASE_URL; function flattenContent(content) { if (typeof content string) return content; if (!Array.isArray(content)) return ; const parts []; for (const block of content) { if (block.type text) parts.push(block.text || ); else if (block.type tool_result) { const inner flattenContent(block.content); parts.push(block.is_error ? 执行失败${inner} : inner); } } return parts.join(\n).trim() || 无内容; } function convertTools(tools) { if (!Array.isArray(tools)) return undefined; return tools .filter((t) t t.name) .map((t) ({ type: function, function: { name: t.name, description: (t.description || ).slice(0, 200), parameters: sanitizeSchema(t.input_schema || { type: object, properties: {} }), }, })); } function sanitizeSchema(schema) { // 只保留最朴素的关键字其余全删 const keep [type, properties, required, items, enum, description]; const out {}; for (const k of keep) { if (schema[k] ! undefined) out[k] schema[k]; } if (out.type object out.properties) { for (const key of Object.keys(out.properties)) { out.properties[key] sanitizeSchema(out.properties[key]); } } return out; } function buildUpstreamBody(body) { const messages []; if (body.system) { messages.push({ role: system, content: flattenContent(body.system) }); } for (const m of body.messages || []) { if (m.role user) { const toolResults Array.isArray(m.content) ? m.content.filter((b) b.type tool_result) : []; if (toolResults.length 0) { for (const tr of toolResults) { messages.push({ role: tool, tool_call_id: tr.tool_use_id, content: flattenContent(tr.content), }); } // 同一条消息里可能还有用户输入 const texts m.content .filter((b) b.type text) .map((b) b.text) .join(\n) .trim(); if (texts) messages.push({ role: user, content: texts }); } else { messages.push({ role: user, content: flattenContent(m.content) }); } } else if (m.role assistant) { const blocks Array.isArray(m.content) ? m.content : []; const text blocks.filter((b) b.type text).map((b) b.text).join(); const calls blocks .filter((b) b.type tool_use) .map((b) ({ id: b.id, type: function, function: { name: b.name, arguments: JSON.stringify(b.input || {}) }, })); const msg { role: assistant, content: text || }; if (calls.length) msg.tool_calls calls; messages.push(msg); } } const out { model: process.env.UPSTREAM_MODEL, messages, max_tokens: Math.min(Number(body.max_tokens) || 4096, 8192), stream: !!body.stream, }; if (typeof body.temperature number) { out.temperature Math.max(0, Math.min(body.temperature, 1)); } const tools convertTools(body.tools); if (tools tools.length) out.tools tools; if (Array.isArray(body.stop_sequences) body.stop_sequences.length) { out.stop body.stop_sequences.slice(0, 4); } return out; }这段代码里有几个决定是踩过坑之后才加的值得单独说。flattenContent里对空字符串做了兜底返回“无内容”。这不是洁癖是因为真的遇到过对端拒绝空 content 的情况。sanitizeSchema是白名单而不是黑名单。黑名单删掉additionalProperties、$ref等看起来省事但只要对端升级一次、多支持一个关键字你的黑名单就漏了。白名单虽然可能丢掉一些有用信息但稳定性高得多。max_tokens那里用了Math.min双向收敛既防止 CLD 助手设得太大也防止没设置的情况。stop_sequences截到 4 个。有的服务对停止词数量有上限超了报 400截断比报错好。4.3 日志怎么打才有用转换层不打日志等于闭着眼睛开车。但日志打太杂也没用会被淹没。我一般只打三样东西而且都做脱敏第一样入站请求的关键结构摘要。不打全文只打这几个数消息条数、每条的角色序列比如system,user,assistant,tool,user、工具数量、max_tokens的值、tools里每个工具的名字和 schema 顶层键。这个摘要能覆盖九成的结构问题。第二样出站请求体的完整 JSON。这个必须打因为 400 的时候你需要拿它去和对端文档逐字段比对。但要落文件不要打屏而且要按时间戳命名方便回溯。第三样上游响应的状态码和响应体前 500 字符。有时候 400 的响应体里藏着关键线索比如具体哪个字段名只是被格式化得很难看。function logSummary(tag, body) { const msgs body.messages || []; console.log([${tag}], { roles: msgs.map((m) m.role).join(,), count: msgs.length, max_tokens: body.max_tokens, tools: (body.tools || []).map((t) t.function?.name || t.name), has_system: msgs.some((m) m.role system), }); }这套日志在排查“同样配置有时通有时不通”的问题时特别管用因为你能直接对比成功和失败两次的摘要差异。4.4 冒烟测试清单转换层写完别急着上真实会话。按下面这个清单一项一项过每项都独立验证出问题能立刻定位。步骤验证内容预期结果失败时的第一怀疑点1纯文本一问一答返回 200有回复模型名、密钥、base URL2带 system 消息返回 200回复符合人设system 搬运位置3带一个无参数工具返回 200tools 嵌套结构4带一个多参数工具返回 200schema 关键字5模拟一次完整工具往返返回 200模型能理解结果tool_call_id 对齐6带长历史20 轮以上返回 200上下文超长7带流式能正常逐块输出流式格式转换第 5 步是最容易翻车的也是最关键的。CLI 助手的实际使用中工具往返占了绝大多数请求。这一步不通前面全白搭。5. 排查实录三个真实卡点与一张速查表理论讲完说几个我当时真卡住的地方。这些从文档里基本看不到但实际遇到的时候特别耗时间。5.1 卡点一同样的配置重启后时好时坏这个最邪门。早上跑得好好的下午重新打开终端就 400配置一个字没改。后来发现是模型名的问题。CLI 助手有个本地配置文件会缓存上一次用的模型名我在环境变量里改了但配置文件里的旧值优先级更高。表现出来就是“有时候生效有时候不生效”——取决于它是从环境变量读还是从配置读。教训是改完配置一定要在 CLI 助手里看一眼当前生效的模型名通常有个/status或/config之类的命令别信环境变量。两边不一致改环境变量等于白改。5.2 卡点二工具描述里有中文引号这个坑我怀疑很少有人遇到但确实存在。某个工具的 description 里我写了一段带中文书名号的说明。对端校验时把它当成了非法字符返回 400而且报错只说“invalid tool definition”。排查方式是逐工具二分定位到具体工具后再逐字段删。删到 description 那一段好了才反应过来是字符问题。后来我把所有工具描述都改成纯 ASCII 加常见中文标点问题再没出现。这件事给我的启发是排查 400 的时候不要默认“我的内容是对的”先假设“我的内容里有对端不接受的东西”然后逐个剥离。心态不一样效率差很多。5.3 卡点三assistant 消息 content 为空CLI 助手在“纯工具调用”的那一轮assistant 消息可能只有tool_use块没有文本块。转成 OpenAI 风格后就是{role: assistant, content: , tool_calls: [...]}。这个结构在多数实现里是合法的但有的服务要求content不能为空字符串必须给null或者干脆省略这个字段。我试了三种写法最后发现省略最保险const msg { role: assistant }; if (text) msg.content text; if (calls.length) msg.tool_calls calls;而不是先给content: 再覆盖。这个差别看起来微不足道但确实解决了一类 400。5.4 400 错误速查表下面这张表是我自己攒的遇到 400 先对照着看大部分能直接命中。报错里的关键词大概率原因快速验证方式改法model not found/invalid model模型名不对用最简请求直接打对端照文档逐字符复制max_tokens/length输出上限越界把max_tokens设成 1024 再试降到对端上限以下tool/function/schema工具定义不合规单工具逐个试白名单化 schemacontent/message消息结构或空内容只发一条纯文本消息补空内容兜底、检查角色序列temperature/parameter采样参数越界删掉所有采样参数夹到 [0,1] 或直接不传context/too long上下文超长新开会话重试清历史、调小窗口role/sequence角色交替不合法打印角色序列看有没有连续同角色拆开或补一条 assistant什么都不说纯invalid request有未知字段被透传打印出站完整 JSON 逐字段核白名单构造请求体5.5 几条压箱底的经验别信“兼容”两个字。所谓的 OpenAI 兼容接口兼容程度差异巨大有的只兼容了最基础的对话工具调用、流式、多模态全都是半成品。上手第一件事不是读文档而是拿一个带工具的请求实测一遍看它到底兼容到哪一层。先把stream关掉。流式模式下的错误信息经常被截断而且流式和非流式走的代码路径有时不一样同一个请求非流式能通、流式就挂。排查阶段先用非流式把结构跑通最后再开流式。转换层要做幂等。同一份请求体喂进去两次输出应该完全一致。如果你的转换里有随机 id 生成、有依赖时间戳的逻辑排查时会非常痛苦因为每次结果都不一样。留一个“旁路模式”。我在转换层里加了个开关打开之后不做任何改写原样转发。这个开关在判断“到底是对端的问题还是我转换的问题”时特别好用——旁路也 400那就是对端或者原始请求的问题旁路能通那就是我转换写错了。二分的方向立刻就明确了。记录每一次成功的请求体。建个目录把跑通的请求体存下来出问题的时候拿它当基线一次只改一个字段做对比。这比从零构造快得多也更不容易漏。对端升级后一定要重跑冒烟清单。这个我吃过亏。对端悄悄收紧了一次 schema 校验之前跑得好好的工具定义突然开始 400而且没有任何通知。养成习惯每次感觉“莫名其妙就不行了”先跑一遍那七步清单比什么都快。最后分享一个我自己的判断习惯遇到 400我会先问自己一句——这个请求体如果我是对端的开发者我会接受吗站在校验方的角度过一遍字段往往能自己发现问题比如某个字段类型对不上、某个数组里有空对象、某个必填项缺失。这种“换个位置看”的习惯比记住一百条报错信息管用。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。