1. 先把一次调用拆开看:接口返回的到底是什么
调用大模型API这件事,表面上看就是往一个地址POST一段JSON、拿回一段JSON,任何一个刚入门的开发者十分钟就能跑通第一个demo。但我带过几批人做业务接入,真正上线之后出问题的,几乎从来不是"能不能调通",而是"返回输出结果为什么和预期不一样"——返回空字符串、返回被截断的半句话、返回带一堆markdown围栏的伪JSON、流式模式下文字一段一段往外蹦还夹着乱码、账单跑得比业务量快三倍。这些问题全都在"调用"和"拿到可用输出"之间的那条缝里。
我把这条缝拆成六个环节:请求组装、连接与超时、响应解析、结果清洗、异常兜底、成本与观测。这篇文章就按这六个环节往下走,讲清楚每一步该怎么写、为什么这么写、哪些参数是必须显式指定的、返回结构里哪些字段是必须检查而不能直接信任的。适合两类人看:一是刚接手业务接入、准备把demo改成生产代码的开发者;二是已经在用、但被截断和幻觉反复折腾、想系统梳理一遍取数链路的人。代码示例我统一用Python写,接口路径和字段名按照目前业界最常见的对话补全风格来,换任何一家服务商基本都能对得上,改动量通常只在base_url和模型名。
1.1 从"发个请求拿段文本"到"拿到可靠结果"
先说清楚我在整篇文章里的基本立场:调用大模型API的难点不在发送,而在接收端的确定性。传统后端接口是契约式的,字段、类型、取值范围都在文档里写死,返回什么你心里有数。大模型接口不是,它是一个概率系统的外壳,同一个请求发两次,返回的文本可以完全不同;返回结构的骨架是稳定的,但里面的内容、长度、甚至是否为空都不稳定。
这就导致一个很反直觉的结论:你要写的防御代码,比业务代码多。我见过太多人把API返回的content字段直接拼进前端、直接入库、直接喂给下一道流程,结果上游偶尔返回一个空字符串,整条链路静默失败,日志里连个报错都没有,排查花掉一整天。实际上接口的返回体里明明白白带着结束原因、用量统计、请求标识,只要多读两个字段就能提前发现异常。
所以我建议的接入姿势是:先写"请求构造器"和"响应校验器"两个独立函数,业务逻辑只调用校验后的结果。构造器负责把消息数组、参数、超时、重试配置组装成一个完整的请求描述;校验器负责检查返回是否可用、是否被截断、是否命中了内容过滤、用量是否超预算。两者都不含业务语义,可以被任何业务复用。这个分层看起来只是多写了五十行,但它把所有不确定性收拢到了两个地方,后续任何异常你只需要盯住这两个函数。
1.2 六个环节的分工与取舍
我先把六个环节的职责摆出来,后面每个章节展开其中两三个:
| 环节 | 核心职责 | 常见失误 |
|---|---|---|
| 请求组装 | 消息数组、参数、结构化约束 | 参数省事不写,用默认值 |
| 连接与超时 | 连接超时、读超时、总耗时上限 | 只设一个超时,流式模式必崩 |
| 响应解析 | 取字段、拆流、处理半包 | 直接信任第一个choice |
| 结果清洗 | 去围栏、补JSON、长度校验 | 用正则硬抠,抠出错误结构 |
| 异常兜底 | 重试、降级、熔断 | 无差别重试4xx,越重试越糟 |
| 成本与观测 | token记账、日志、留痕 | 只记业务结果,不记用量 |
这张表我建议直接贴到项目README里,因为它是"接入检查清单":六个环节任何一个没有对应代码,就说明这条链路有裸露的风险点。我自己在评审别人的接入代码时,基本就是按这六行逐条问:超时设了吗?流式半包处理了吗?429怎么退避?用量字段读了吗?通常问到第三行就能发现至少两个缺口。
2. 请求体里每个参数到底在干什么
请求组装这一块,最容易踩的坑不是写错,而是"不写"。很多接口为了降低上手门槛,把大量参数设计成有默认值、可以省略,于是第一版代码往往只有一个model和一个messages,跑起来还挺正常。但默认值是面向通用场景调优的,你的场景未必通用。等到业务方反馈"输出太跳""同样的输入两次结果差太多""偶尔被截断",你再回头一个个补参数,成本比一开始就写清楚高得多。
我的习惯是:不管接口文档怎么写,我的请求构造器里永远显式列出模型名、消息数组、采样参数、长度上限、超时这五项,其余全部按需。这样任何一次线上异常,你看一眼请求日志就知道当时到底用了什么参数,而不是靠猜"默认温度是多少"。下面按消息、采样、长度三段拆开讲。
2.1 消息数组与角色设计
消息数组是请求体的核心,顺序和角色分配直接决定输出质量。最常见的风格是三个角色:system负责设定身份与规则,user负责本次输入,assistant用于多轮历史或"预填充"。我见过两种极端写法,都不太可取:一种是把所有内容都塞进user,包括"你是一个资深审稿人,请按以下规则……",结果规则和输入混在一起,模型有时把规则当成了待处理文本;另一种是system写了两千字,事无巨细,反而稀释了真正关键的三条约束。
我的经验是:system控制在三百字以内,只放不可协商的约束,比如输出语言、输出格式、禁止事项、必须遵守的口径。具体的任务说明、参考资料、待处理内容,全部放user,并用明确的分隔标记隔开。分隔标记不要用花哨的符号,用清晰的文本标签就好,比如在一段待总结的正文前后各放一行固定的标记行。这样做的好处是,当你需要把这段输入换成另一个业务的输入时,只需要替换标记之间的内容,system完全不用动。
多轮对话还有一个坑:历史上限。如果把整个会话历史原样带上,几十轮之后请求体就膨胀到超出上下文窗口,接口直接报参数错误。我的处理方式是按轮数加长度双重裁剪:保留最近若干轮,并且从最老的一轮开始检查累积token估算值,超出预算就丢弃整轮,绝不从中间截断一条消息——截断成半句话的历史消息对模型来说是噪音,比没有更糟。
注意:assistant角色的"预填充"是个很实用的技巧,比如你想让模型直接输出JSON,可以先放一条assistant消息内容是左花括号,强制它从这里续写。但这依赖具体接口实现,接入前务必先用最小请求验证,别在生产环境直接赌。
2.2 温度、top_p与惩罚项的实际手感
采样参数这一组,文档里通常写成0到2、0到1的浮点数,看数字完全没感觉。我给一个我自己的对照表,都是实际调过之后的手感:
| 场景 | 温度 | top_p | 说明 |
|---|---|---|---|
| 结构化抽取、分类、字段填充 | 0 到 0.2 | 1 | 越确定越好,允许重复 |
| 摘要、改写、客服回复 | 0.3 到 0.6 | 0.9 | 保留一点措辞变化 |
| 头脑风暴、文案发散 | 0.8 到 1.0 | 0.95 | 再高开始出现事实漂移 |
| 需要严格复述原文 | 0 | 1 | 配合低长度上限 |
温度和top_p不要同时大改,这是很多人调参调不明白的原因。这两个参数本质上都在控制候选词的截取范围,一起往上推等于双重放大随机性,结果就是"每次跑都不一样,且都不太好"。我的做法是固定top_p为一个常规值,只调温度,一次调0.1,对比五组输出再决定。
至于频率惩罚和存在惩罚,我个人的态度是能不用就不用。它们会干扰模型对专有名词和固定格式的复现——你可能只是想让它别啰嗦,结果它把该重复出现的字段名也换了个说法,结构化输出直接崩掉。如果确实遇到重复啰嗦,优先改prompt里的约束,而不是加惩罚项。
2.3 max_tokens与截断的账要提前算
这一项是我认为最该显式指定、却最常被省略的参数。省略的后果是:接口用默认上限,一旦模型进入"长篇大论"模式,你的返回就停在一句话中间,而你的代码如果不检查结束原因,就会把半句话当成完整结果入库。
正确的算法是先估后定:输出上限 = 预期输出长度 × 1.5 + 余量。比如你要模型返回一个包含五个字段的JSON,每个字段平均二十个字,加上键名和括号,实际输出大概两百个token,那你设300就够,设500更稳。设太大会有两个副作用:一是部分服务商按上限预留资源,响应变慢;二是模型真的可能写满,白花输出费用。
同时必须检查返回体里的结束原因字段。它的取值通常几类:正常结束、达到长度上限、命中内容过滤、工具调用。只有第一种可以直接用;第二种说明被截断,要么重发并提高上限,要么把已有内容当作部分结果处理;第三种说明内容被拦,需要看具体原因记录日志;第四种说明模型想调工具,你如果不支持工具调用,就该把它当成协议错误处理而不是解析正文。这四种分支不写全,等于把不确定性放进了数据库。
2.4 输入侧的token预算怎么估
输出要算上限,输入同样要算,尤其是你要把长文档、检索片段、历史对话拼进去的时候。我的粗算规则是:中文按字符数乘以1.5估token,英文按字符数除以4估,代码和符号混排的文本按字符数乘以0.8估。这个估法肯定不精确,但误差方向是保守的(偏大),用来做"是否超限"的判断足够了。
真正精确的做法是调用服务商提供的计数接口,但那需要额外一次网络往返,不适合放在热路径上。我的折中是:常规请求用粗估,只有接近上下文上限(比如估算值超过窗口的80%)时才调用精确计数接口,做一次最终确认。这样既不会每次都多花一次往返,也不会因为粗估误差把请求打爆。
3. 返回结果的解析:从JSON到能用的文本
拿到响应之后,很多人的代码就一行:取choices数组第一个元素里的消息内容。这一行能跑通90%的情况,剩下10%就是线上事故。我把解析分成三层:结构层防御取值、流式层自己管缓冲、内容层做格式兜底。三层缺一层,你都会在某个深夜被叫起来看日志。
3.1 非流式响应结构与防御式取值
一次典型的非流式响应,骨架大概长这样:顶层有请求标识、模型名、创建时间、用量统计,中间是choices数组,每个元素包含序号、消息体、结束原因,消息体里又有角色和内容。另外可能还有内容过滤的标记、系统指纹之类的附加字段。
防御式取值的意思是:每一层都用带默认值的方式取,并对关键字段做显式校验。先检查choices是不是非空数组,不是就当成异常响应;再检查第一个元素的结束原因是不是正常;再检查内容是不是非空字符串;最后检查用量字段里的输入输出token是否在预算内。这四步加起来不到二十行,但能挡掉绝大多数"静默失败"。
还有一个细节:不要假设choices一定只有一个元素。有些接口支持一次生成多个候选,即使你没主动要求,某些实现也可能返回多条。稳妥写法是取第一个,同时把候选数量记进日志,如果长期大于1说明你的参数被别处覆盖了,值得查一查。
用量字段也值得单独说。它通常给出输入token、输出token和总量,这是你做成本核算的唯一可靠来源。不要用自己估的token数去记账,估算误差在长文本上能到30%,月度账单对不上就是这么来的。每次调用都把这三个数字连同请求标识一起落库,后面做成本分析才有依据。
3.2 流式SSE拆包:缓冲区必须自己管
流式返回是我认为最容易写出事故的地方。原理上,服务端把结果切成一个个事件块,用固定的分隔符串起来,通过一条长连接持续推送。客户端读到的不是完整事件,而是字节流,可能半个事件、可能三个半事件,取决于网络分包。
所以正确写法必须包含三件事:累积缓冲区、按分隔符切分、保留残余。常见的错误实现是每读到一块就直接按行处理,结果就是偶尔出现"data: {"这个半截被当成一条事件,解析失败抛异常,或者更糟——解析成功但内容丢失。我的实现逻辑是:把新读到的字节解码后追加到缓冲区,然后循环查找分隔符,找到就把前面的部分切出来处理,剩余的留在缓冲区等待下一次读取。连接结束时如果缓冲区还有残留,也要尝试处理一次,别丢掉最后一段。
第二个坑是编码。流式读取拿到的是字节,如果你用固定长度去截取解码,一个多字节汉字可能被从中间切开,就会解出乱码。解决办法是用增量解码器,或者干脆只在分隔符边界上做解码——按字节切分事件、按事件整体解码,能天然避开这个问题。我踩过一次,输出里每隔几十字就夹一个问号,排查了半天才发现是解码边界的问题。
第三个坑是结束标记。流式流的最后一个事件通常是固定的结束信号,或者一个带结束原因的事件。不要靠"连接关闭"来判断生成完毕,因为网络抖动也会导致连接关闭,这时候你无法区分是正常结束还是中断。正确做法是:只有读到结束事件,才标记本次调用成功;连接在没读到结束事件的情况下关闭,一律按中断处理,可以重试或按部分结果处理。
3.3 结构化输出与JSON抽取的兜底
让模型返回JSON是最常见的需求,也是最常见的翻车点。即使你在prompt里写了"只返回JSON,不要任何解释",模型仍有概率给你加一段"好的,以下是结果:",或者用三个反引号包起来,或者在JSON后面补一句说明。
我的处理分三层递进。第一层,优先用接口本身的结构化输出能力——现在多数服务商都支持指定返回格式为JSON对象,或者在请求里传一个schema,服务端会保证返回可解析。这一层能用就用,成本最低、稳定性最高。
第二层,如果接口不支持,就在解析时做括号配对抽取:找到第一个左花括号,然后逐字符扫描,维护一个深度计数器,遇到左括号加一、右括号减一,归零时截断。这比正则可靠得多,因为它能正确处理嵌套对象和字符串里的括号。正则匹配到第一个右括号就停,嵌套结构必崩。
第三层,解析失败后的补救:把原始输出连同"请修复为合法JSON"的指令再发一次,也就是所谓的自修复重试。我一般限制最多一次,因为两次修不好说明模型本身没理解任务,继续重试只是浪费钱。
注意:抽取出来的JSON还要做字段校验,别以为能解析就完事。必填字段是否存在、类型是否对、枚举值是否在允许范围内,这些都要检查。我见过接口返回了合法JSON但字段名是另一个语言的,下游直接空指针。
4. 稳定性工程:超时、重试、限流与并发
前三章讲的是"怎么拿对",这一章讲"怎么在异常情况下还能拿对"。生产环境和本地调试最大的区别在于:本地你只跑一次,生产你每秒跑十次,任何小概率异常都会被放大成必现故障。稳定性这块我踩过的坑,基本都集中在超时设置和重试策略这两个看起来最简单的地方。
4.1 超时时间怎么设
很多人的代码里只有一个超时参数,或者压根用底层库的默认值。这在非流式场景下勉强能用,但在流式场景下会出事:流式连接的"总耗时"可能长达几十秒,而"两段数据之间的间隔"通常只有几百毫秒。你用总耗时去设超时,要么太短把正常请求掐死,要么太长导致真卡住的时候要等很久才发现。
正确的做法是拆成三个:连接超时、读取间隔超时、总耗时上限。连接超时给3到5秒,用于快速失败掉连不上的情况;读取间隔超时给30到60秒,覆盖模型思考最久的那一段;总耗时上限按你允许的最大输出长度估算,比如输出上限2000 token、生成速度按每秒30 token算,理论最长约67秒,那你设90秒比较稳妥。
这个估算里有个变量要留心:首字延迟。有些模型在输出第一个字之前会有一段较长的准备时间,尤其是输入很长的时候。如果你的读取间隔超时设得太短,第一个事件还没来就超时了。我一般把首字等待单独放宽一档,或者干脆用读取间隔超时去覆盖它,宁可等久一点,别把正常请求当故障。
4.2 重试策略:只重试该重试的
重试的第一原则是看状态码分类。超时、连接重置、5xx、429,这些值得重试;401、403、404、400这类,重试一万次也是同样的结果,只会把问题拖成雪崩。我在代码里用一个白名单判断,白名单之外一律不重试,直接上报。
第二原则是指数退避加随机抖动。固定间隔重试在限流场景下是灾难:你被限流了,等一秒再打,结果同一时刻被限流的一百个请求一起醒来,又打满一次。退避的间隔按1秒、2秒、4秒递增,再叠加一个0到1秒的随机抖动,能把请求打散。重试次数我一般设2到3次,加上总耗时预算限制,避免一个请求重试到天荒地老。
第三原则是区分流式和非流式。非流式请求重试很简单,失败了整体重发。流式请求就麻烦:如果你已经往外吐了一半内容,重试会导致内容重复。我的处理是:流式场景只在"还没收到任何内容"时重试,一旦开始输出就不再重连,改为把已输出内容标记为不完整,由上层决定是否展示。这个判断需要在流式处理器里维护一个"是否已产出"的标志位。
第四原则是幂等性。如果这次调用会产生副作用(写库、发消息、扣款),重试前必须先确认上一次是否已经生效。最常见做法是带一个业务侧生成的请求标识,落库时做唯一约束,重试撞上了就跳过写入。
4.3 并发控制与限流
并发这块,很多团队的教训是:本地压测很稳,上线后被限流打回。原因是接口的限额通常按两个维度算——每分钟请求数和每分钟token数。前者你容易注意到,后者经常被忽略:一批长文本请求,请求数没超,token数早就爆了。
我的做法是在客户端做两个令牌桶,一个按请求数、一个按预估token数,取两者中更严格的作为放行条件。token桶的容量按你账号的配额打个七折设置,留出余量给突发。这样即使服务端限额临时调整,你也不会立刻撞墙。
另外强烈建议加熔断。连续N次失败(N取5到10)就短暂停止打这个接口,比如停30秒。没有熔断的话,服务端一次抖动会引发你的重试风暴,重试风暴又加剧服务端抖动,最后变成自我实现的雪崩。熔断的恢复用半开模式:放一个请求试探,成功就恢复,失败就继续停。
5. 成本与可观测性:把每一笔都记清楚
接口调用的成本是隐性的,它不像服务器那样有明确的账单日提醒,而是随着业务量悄悄上涨。我见过一个团队月成本翻了四倍才发现问题,原因只是某次改动把历史对话全量带上了,每轮请求的输入token涨了十倍。要避免这种情况,唯二的办法是精细记账和定期复盘。
5.1 token核算与成本换算
核算的基准永远是接口返回的用量字段,不是你的估算。把每次调用的输入token、输出token、模型名、业务标识记下来,按天聚合。成本换算公式很朴素:单次成本 = 输入token数 ÷ 1000000 × 输入单价 + 输出token数 ÷ 1000000 × 输出单价。注意输出单价通常比输入贵好几倍,所以优化重点往往在压缩输出而不是压缩输入。
举个具体例子:某业务每天调用一万次,每次平均输入800 token、输出400 token,假设按某服务商的常见价格区间,输入部分和输出部分各算一遍,一天的成本大概在几元到几十元这个量级。单看不吓人,但如果你的输入里塞了全量历史,平均输入涨到8000 token,成本就是十倍。这时候你会突然意识到,前面说的历史裁剪策略不只是为了不超上下文,更是直接的成本控制手段。
几个实际有效的省钱手段:缓存(相同请求直接返回上次结果,注意只在温度接近0时安全)、分级模型(简单任务用小模型,复杂任务才上大模型)、输出压缩(prompt里明确"只返回结果不要解释")、输入去重(多轮对话里重复出现的系统提示只发一次)。缓存这一条我要强调,它是最容易被忽略但收益最大的:很多业务场景的输入是高度重复的,比如固定的分类任务、固定的模板填写,命中率能到四五成。
5.2 日志字段怎么设计才算够用
日志设计的原则是:线上出问题时,你不用重跑就能复现当时的现场。基于这个原则,我必记的字段有这么一组:
| 字段 | 用途 |
|---|---|
| 请求标识 | 关联服务端排查,一对一对账 |
| 业务追踪号 | 串起上下游,定位影响范围 |
| 模型名与版本 | 排除模型切换导致的行为变化 |
| 关键参数快照 | 温度、上限、是否流式 |
| prompt版本号 | 判断是否为prompt改动引入 |
| 输入输出token | 成本分析、异常用量告警 |
| 首字延迟与总耗时 | 性能劣化预警 |
| 结束原因 | 区分截断、过滤、正常 |
| 重试次数与最终状态 | 稳定性指标 |
| 输出内容摘要 | 不要全量落,存前若干字符加长度 |
prompt版本号这个字段很多人不加,我觉得它价值极高。你改了prompt,输出风格变了,没有版本号你根本不知道是哪次改动引起的。我的做法是把prompt存成文件、按内容哈希生成版本号、每次请求带上,改了文件哈希自动变,零维护成本。
输出内容要不要全量落库?我的建议是默认不全量。一是存储成本,二是内容可能包含业务数据,落库意味着扩散风险。折中方案是存摘要:长度、前若干字符、哈希值。真要排查时用请求标识去服务商侧查,或者用哈希做去重统计。
6. 常见问题与排查实录
这一章是我这几年被问得最多的一类问题的汇总。排列顺序按我遇到的频率从高到低,每条都给出表现、原因、处理三步。建议直接收藏做速查表用。
6.1 典型报错对照表
| 表现 | 最可能的原因 | 处理方式 |
|---|---|---|
| 401 鉴权失败 | 密钥尾部有空格、环境变量没加载、密钥已轮换 | 打印密钥长度和前几位,确认加载路径 |
| 404 找不到 | 接口路径拼错、模型名不在可用列表 | 用最小请求验证路径和模型名 |
| 400 参数错误 | 消息角色顺序非法、上限超窗口、字段类型不对 | 逐字段对照文档,先跑最小请求 |
| 429 限流 | 并发太高或token配额打满 | 指数退避,检查token桶 |
| 5xx | 服务端临时异常 | 白名单重试,加熔断 |
| 返回空内容 | 结束原因非正常、命中内容过滤 | 读结束原因字段,检查输入是否触发拦截 |
| 输出戛然而止 | 长度上限太小 | 检查结束原因,提高上限重发 |
| 中文乱码 | 流式解码边界错误 | 按事件整体解码,用增量解码器 |
| 结构解析失败 | 模型加了说明文字或围栏 | 括号配对抽取,或开启结构化模式 |
| 同样的输入结果差异大 | 温度过高、未固定随机种子 | 降到0.2以下,固定参数 |
排查这类问题的通用心法是:永远先看原始响应,不要看你封装后的结果。我见过太多次,排查一圈最后发现是自己的解析函数把正常响应处理坏了。所以在封装层里加一个"原始响应留存"的开关,出问题时打开,比什么都快。
6.2 内容层面的坑:幻觉、漂移与过度自信
报错类问题好查,因为它是显性的。真正折磨人的是内容层面的问题,接口返回200、结构完全合法、内容却是错的。
第一种是事实性幻觉。模型会把不存在的信息说得像模像样。应对方法不是反复强调"不要瞎编"——这句话的效果很有限——而是改变任务形态:让它基于给定材料作答,并且要求"材料中没有提到的,直接回答没有提及"。我实测下来,这个约束比任何"请准确回答"都管用。
第二种是格式漂移。同一段prompt,跑一百次有三次格式不对。这类问题靠prompt是压不到零的,必须靠代码兜底:解析失败就当异常,重试一次,还不成就走降级路径。我的接受阈值是失败率低于1%,超过这个数就要回头查是不是prompt里的约束互相冲突了。
第三种是长度漂移。让它写两百字,它给你三百五。这个除了在prompt里给出字数范围,更有效的是用长度上限做硬截断,然后在提示里说明字数区间的两端。硬截断别直接切字符串,那样会切在句号中间,最好在标点上做就近回退。
第四种是过度自信的语气。模型很少说"我不确定"。如果你的业务场景需要不确定性表达(比如风险判断、医疗健康类的辅助信息),就必须在prompt里明确要求给出置信度或"无法判断"的选项,并在渲染层做相应呈现,不能把模型的陈述直接当结论。
6.3 我自己踩过的几个坑
说几个具体到我自己的、不太会写在文档里的教训。
第一个是不要把接口调用的超时设得比上游调用方还长。我做第一个版本时,服务端调模型设了120秒超时,但前端网关的超时是60秒。结果就是模型慢慢想,前端已经报错了,我的服务还在那儿跑,跑完再写库,写进去一条用户根本没看到的结果。后来统一规则:下游超时必须短于上游,逐层递减,并且在下游超时的时候不做补偿性写入。
第二个是流式响应的"看起来正常"陷阱。我曾经以为流式只要不抛异常就算成功,直到有一次网络在末尾抖了一下,连接正常关闭、没有结束事件,但我没有检查,存进去一段少了两句话的文本。后来我给流式解析加了三个断言:必须收到结束事件、必须收到用量字段(很多实现只在最后一块给用量)、内容长度必须大于零。三个断言缺一个就标记为不完整。
第三个是参数默认值会变。这个最阴。服务商的默认参数不是永久不变的,升级版本时改个默认温度或默认上限,你的输出就会莫名其妙漂移,而你的代码一行没动。所以我在请求构造器里坚持显式写参数,就是为了对抗这个。写死参数不算坏习惯,在这里是防御。
第四个是别在重试里做状态变更。早期我把"写入结果表"放进了重试的闭包里,结果 429 重试三次,写了三条重复记录。现在所有带副作用的操作都放在调用成功之后、且带业务唯一键。
最后一个心得,关于测试:给模型调用写测试用例,断言不要写具体文本,写结构。断言"返回值包含五个字段、字段类型正确、长度在区间内、结束原因是正常",这类测试稳定且有意义。如果你断言具体字符串,那这个测试每次模型微调都会红,最后所有人都会把它删掉——这是我见过最典型的测试反模式。
接入这半年下来,我给新人的建议始终是同一句:把它当成一个"会返回语义内容的外部接口",而不是一个"确定函数"。确定函数你只需要管输入;语义接口你要管输入、管输出、管输出之后的一切。真正花时间的从来不是那行POST,而是后面那一整套收口的功夫。