夜雨聆风学习资料网

ARTICLE · 1058368

文档写着integer,它传了string,然后它说“可能参数名拼错了“

文档写着integer,它传了string,然后它说“可能参数名拼错了“

这个"病"长什么样

调API这件事,人类新手和AI新手犯的错一模一样,但人类新手被报错怼几次就长记性了。

AI不长记性。它每次都像第一次见到这个接口一样,热情地、自信地、凭"印象"把参数填进去。

你让它对接一个第三方接口,它三秒钟给你写出调用代码,格式漂亮、注释齐全、结构90分。你一跑,报错。接下来你能看到这个系列里最经典的一组画面:

  • 文档明明白白写着 user_id 是整数,它传了字符串 "10086",接口回 422
  • 字段名文档写的是 order_no,它凭记忆写成 orderNum,接口回 字段缺失
  • 金额单位文档写的是,它传了 69.9,接口当成 69.9 分,用户实付 7 毛钱
  • 时间要 ISO8601 带时区,它传了 "2026/9/20 下午3点",接口直接解析失败
  • 枚举值只接受 "PAID"/"UNPAID",它传了 "已支付"

每个错单独看都蠢得离谱。但真正让你血压升高的不是这些错,是它面对报错时的反应——它不读错误信息,先猜。

接口说:{"code":422,"detail":"user_id must be a valid integer"},这已经把病因喂到嘴边了。AI的反应是:

“这个报错可能是参数名拼写有误,建议检查 user_id 是否应为 userId 或 uid。我来尝试修改为驼峰命名……”

它把一个类型错误,诊断成了命名错误,然后带着你的代码朝一个完全错误的方向狂奔。 你不拦着,它能把同一个接口的字段名穷尽式地改一遍——userIduiduseriduserID——每改一次跑一次,每跑一次错一次,半小时过去了,正确答案(把引号去掉)从头到尾没出现在它的候选里。

像什么呢?AI调API像一个从不看说明书、全靠手感组装家具的人:螺丝孔对不上,他不反思是不是拿错了零件,先怀疑是不是厂家孔打歪了,然后拿着锤子硬砸。 你站在旁边,说明书就摊在桌上第3页写着"此孔需用B号螺丝",他不看。


我的真实案例

我给一个客户做ERP对接的小工具:客户那边有套老系统,开放了一套HTTP接口,要把我们这边的订单数据每天同步过去。接口文档是一份有点年头的Swagger(OpenAPI),字段不算多,但类型和命名风格非常不统一——有的字段下划线,有的驼峰,金额有的按元有的按分,这是后面所有坑的温床。

我把文档链接和接口说明给AI,让它先写一个"创建订单"的调用。

第一次交付:自信,然后422

AI秒回,代码很漂亮:

import requestsdef create_order(order):    payload = {        "orderNo": order["order_no"],        "userId"str(order["user_id"]),        "amount": order["amount"],           # 69.9        "status""已支付",        "createTime": order["created_at"],   # "2026/9/20 15:00"        "remark": order.get("remark"""),    }    resp = requests.post(        "https://erp.client.com/api/order/create",        json=payload,        headers={"Authorization""Bearer xxx"},    )    return resp.json()

我一跑,422 Unprocessable Entity

{"code":422,"message":"validation failed", "errors":[   {"field":"order_no","error":"required field missing"},   {"field":"user_id","error":"must be of type integer"},   {"field":"amount","error":"must be integer (unit: cents)"} ]}

我把这个报错原样贴回给它。注意,报错信息已经精确到字段和原因了。

第二轮:它开始猜,方向全错

AI的回复:

“接口返回字段校验失败,orderNo 未被识别。这通常是因为参数命名风格不匹配,第三方系统可能要求驼峰转下划线。我尝试将所有字段调整为下划线风格,并对金额做字符串处理……”

然后它交出第二版:字段名改成了下划线(这步歪打正着对了一半),但:

payload = {    "order_no": order["order_no"],    "user_id"str(order["user_id"]),     # 还是字符串!它没动    "amount"str(int(order["amount"])), # 69.9 → "69",直接抹掉小数,还转成字符串    "status""已支付",                    # 枚举还是中文    "create_time""2026-09-20 15:00:00"# 换了格式但还是不带时区、不是ISO    "remark": order.get("remark"""),}

只处理了它"猜"出来的命名问题,对报错里明写的类型问题(integer、单位分)视而不见user_id 外面那个 str() 它从头到尾没意识到是自己加的、还得自己拆。amount 它的处理方式是 str(int(...))——把 69.9 元先砍成 69 再转字符串,类型、单位、精度三个错叠在一起。

我再跑,还是422,这次错误少了一个(order_no过了),但 user_id 和 amount 依然红。

第三轮:它开始"穷尽式改名"

我再次贴上报错。AI的回复开始有了那种熟悉的"甩锅感":

“user_id 字段持续校验失败,可能该接口实际使用的字段名并非文档所示。建议尝试以下候选:uiduseridmemberIduser_id_str……我先逐个尝试。”

它开始把一个类型问题,当成字段名问题做暴力枚举。 这是这个病最危险的阶段——它已经不看文档、不看报错了,进入"碰运气"模式。如果我放任它试,它能把网络请求发几十次,把对方接口的错误日志刷满,而正确答案始终是那个最简单的操作:把 str() 去掉。

到这里我叫停了。三轮,一个接口,没调通,而且越改越偏。

我接手后:对着报错和文档,十分钟

我做了三件AI本该做、但它一件都没做的事:

1. 读报错,按图索骥。 报错说 user_id must be integer——那就是整数,删掉 str(),传 10086 而不是 "10086"。一个引号的事。

2. 读文档的单位说明。 文档在 amount 字段旁边有一行小字"单位:分"。69.9 元 = 6990 分,要做单位换算,并且是整数:

amount_cents = round(float(order["amount"]) * 100)

3. 把每个字段对着文档的类型/枚举逐个核一遍。 status 用英文枚举 "PAID",时间用 ISO8601 带时区:

from datetime import datetime, timezone, timedelta# "2026-09-20T15:00:00+08:00"create_time = datetime.fromisoformat(order["created_at"]).astimezone(    timezone(timedelta(hours=8))).isoformat()

改完一次通过。从头到尾,没有一个错误需要"猜"——文档和报错把所有答案都写好了,AI只是不肯看。

根治:让它先建"契约",再写调用

这次之后我改了工作流。再也不让AI"看一眼文档就直接写调用代码",而是强制它分两步,先固化接口契约:

from pydantic import BaseModel, Field, conintfrom enum import Enumclass OrderStatus(str, Enum):    paid = "PAID"    unpaid = "UNPAID"class CreateOrderRequest(BaseModel):    # 字段名、类型、单位、枚举,全部对着文档显式声明    order_no: str = Field(..., alias="order_no")    user_id: int                              # 整数,不是字符串    amount: conint(strict=True)               # 严格整数,单位:分    status: OrderStatus                       # 只接受 PAID / UNPAID    create_time: str                         # ISO8601 带时区    remark: str = ""    class Config:        populate_by_name = Truedef to_cents(yuan: float) -> int:    return round(yuan * 100)

然后调用前先过一遍模型校验:

payload = CreateOrderRequest(    order_no=order["order_no"],    user_id=int(order["user_id"]),    amount=to_cents(float(order["amount"])),    status=OrderStatus.paid,    create_time=to_iso8601(order["created_at"]),    remark=order.get("remark", ""),)resp = requests.post(url, json=payload.model_dump(by_alias=True))

这一层的意义是:类型错、单位错、枚举错,在请求发出之前就被本地模型拦下,根本到不了服务器。 以前是"写错→发出去→接口422→AI瞎猜→再写错"的死循环,现在是"写错→本地直接报错告诉你哪一格不对→当场改"。报错信息也从第三方接口那段含糊的英文,变成精确到字段的"user_id 应为整数"。


为什么AI会这样?

1. 它写代码靠的是"概率印象",不是"这份文档"

模型生成 userId 还是 user_id、要不要加引号,依据是什么?是它训练数据里"这种接口通常长什么样"的统计印象——大多数REST接口用驼峰、ID经常是字符串,于是它顺着最常见的模式写。

但你对接的是一个具体的、特定的接口,它的约定恰恰可能是小众的、老旧的、反惯例的。 通用印象和具体契约一旦不一致,错的一定是印象。模型的本能是"调用最可能的样子",而正确的工程动作是"照这份文档的确切定义来"——前者是联想,后者是查表,它默认走前者,因为查表需要你把"表"真正喂给它并强制它用。

2. 报错信息它"看到了",但没有真正进入推理

这是最反直觉的一点。你把 must be of type integer 贴给它,字它都认识,但下一轮它的行为显示它根本没把这条约束用上。

原因在于:对模型来说,报错文本和需求描述是"并列的一段话",它没有"先解析错误→定位字段→只改病因"这个固化的调试程序。 它倾向于把报错当成一个模糊的信号,然后用自己最熟练的假设(命名问题、格式问题)去套。人类调试是"假设←证据"的闭环,AI默认是"生成一个听起来合理的假设",证据权重经常被它的先验印象盖过。

3. "改名试错"对它来说成本最低

为什么它一上来就猜参数名?因为改个名字、重发一次,对它是最简单的动作:不用理解类型系统、不用读文档、改一个字符串就行。

模型会系统性地偏好"改动小、看起来在努力、不需要深度理解"的动作。 把 "10086" 改成 10086 只需要删两个字符,但这背后要求它理解"我之前加的 str() 是错的、integer 和 string 的区别在这里是致命的"——这个理解链条比改名字长得多,它倾向于绕开。

4. 它不会为"反复失败"感到不好意思

人类工程师连续三次调不通一个接口,会开始怀疑人生、回头重读文档。AI没有这个反馈机制——它不会因为第五次还报错而下定决心"这次一定先看文档",每一轮对它都是新的、情绪稳定的一次瞎试。 没有你的强制介入,这个循环可以无限持续。


这种病的代价

  • 🔴 接口对接到处是暗雷:类型、单位、枚举、时区、命名风格,任何一个猜错,轻则报错调不通,重则请求被接受但数据悄悄错——金额按分传成元、时区错位8小时,接口不报错,业务对不上账才发现
  • 🔴 调试时间黑洞:一个引号能解决的问题,它能带你绕半小时的"改名枚举"。你盯着它一次次试错,注意力和时间全被吸干
  • 🔴 暴力试错污染对方系统:它为了碰运气连发几十次请求,可能触发对方接口限流、封掉你的IP/账号,甚至在对方的日志系统里留下一堆垃圾记录——对接还没成,先把甲方得罪了
  • 🔴 错误的"修复"会层层叠加:它每轮瞎改留下的 str()、int()、单位换算互相缠绕,代码越改越脏,最后没人说得清 amount 现在到底是元还是分
  • 🔴 你以为它会,其实它在演:AI写调用代码时那种"秒出、自信、结构漂亮"的样子极具迷惑性,让人误以为对接完成。真正的能力不是写出调用的样子,是让请求被服务器接受且数据正确——这两件事之间隔着整套接口契约

怎么治

方法1:强制"先读文档、提取契约",再写代码

把工作流拆成不可跳过的两步,写进给它的指令:

“第一步,不要写调用代码。逐字段阅读这份接口文档,输出一张表:每个字段的名称、精确类型、是否必填、单位、取值范围/枚举、示例值、约束条件。我确认这张表无误后,第二步你再基于它写代码。”

先产出一份"字段契约表",相当于逼它从"凭印象"切换到"照表抄"。接口调试里90%的错,在认真读文档这一步就不会发生。 文档是Swagger/OpenAPI的话,直接让它从 schema 部分提取,那里类型定义最权威。

方法2:用数据模型在本地把参数"焊死"

不要让 payload 是一个随手拼的 dict,用 Pydantic(Python)或同类校验模型,把每个字段的类型、单位、枚举显式声明,请求发出前先 .validate() / 构造模型:

payload_model = CreateOrderRequest(**raw_data)   # 类型不对这里就炸resp = requests.post(url, json=payload_model.model_dump())

好处有三:错误在本地提前暴露、报错精确到字段、文档约定变成了代码里不可绕过的强约束。字段名要对齐外部 alias 的,用 Field(alias=...) + by_alias=True,命名风格也一并锁死。

方法3:报错要它"先解析、后动手",并给出固定调试程序

别只把报错丢给它说"改"。强制它按一个结构化流程走:

"处理这个报错前,先输出三部分再改代码:

  1. 报错精确含义
    :服务器说的是类型/缺失/单位/权限中的哪一种?引用原文;
  2. 定位到具体字段和当前值
    :我们现在传的是什么、文档要求是什么;
  3. 最小修改方案
    :只改导致报错的那一处,不许动无关字段,不许改名字碰运气。 确认方案后再改。"

这一步直接掐死它"跳过分析、上来改名"的本能。好的调试是最小修改、单点验证;坏的调试是广撒网、碰运气。

方法4:单位/枚举/时区这类"静默错"专门设防

最危险的不是报错的错,是不报错的错。金额单位(元/分)、时区、字符编码这类问题接口往往照单全收,错了也不吭声。给它立专门规矩:

## 接口对接检查清单(每次必须逐项确认)- [ ] 金额字段单位:元 or 分?是否做了换算?结果是否为整数?- [ ] 时间字段:格式是否 ISO8601?是否带时区?- [ ] ID/数量字段:类型是 integer 还是 string?有没有多余的 str()/int()?- [ ] 枚举字段:取值是否严格等于文档列表(大小写、中英文)?- [ ] 字段命名:下划线 or 驼峰?是否与文档逐字一致?- [ ] 必填字段是否齐全?嵌套结构层级对不对?

让它交付前对照清单自报一遍。把"凭印象最容易猜错的维度"列成清单,就把它的概率弱点变成了确定性检查点。

方法5:有条件就先跑通"官方示例",再换成真实数据

如果文档带 curl 示例或可运行的样例请求,先让AI原样跑通官方示例(hardcode 示例参数),确认链路、鉴权、字段都对,再一步步把示例值替换成真实变量:

“先用文档里的示例参数,一个字不改,把请求跑通。跑通后我们再逐个字段替换成真实数据,每替换一个验证一次。”

这样一旦报错,你立刻知道是"刚替换的那个字段"的问题,而不是面对一团未知。固定基准、单点变更,是定位一切参数问题的通用方法。


一句话总结

AI调接口的默认模式是"凭训练印象猜参数"——字段名猜、类型猜、单位猜、枚举猜,猜完一跑422,它还不读报错,先把类型错误诊断成命名错误,然后穷尽式改名、暴力碰运气,一个引号能解决的问题绕你半小时。治病的关键是把它从"联想模式"强制切到"查表模式":先逐字段读文档、提取契约表,再用数据模型把类型单位枚举焊死在代码里,报错时逼它先解析证据再做最小修改,并对金额/时区这类"不报错的静默错"专项设防。记住,对接接口时文档和报错已经把所有答案写好了——AI的问题从来不是不够聪明,是太着急表现、不肯先低头看一眼桌上的说明书。

相关学习资料