Ling-3.1-flash 的出参与入参对齐——超时重试写进调用层

文章导读
把参数拼装、返回字段解析、超时与重试收进调用层,是判断「出参与入参对不上」的前提:业务代码只传语义参数、只读归一化结果,字段错位或重试写重了,排查范围能缩到一个文件里。建议的顺序是先抓一次真实收发记录,据此写字段对照表,再补集中拼装、错误分组和幂等键,最后用同一请求重复触发确认没有重复写入。
📋 目录
  1. Ⅰ 抓一次真实调用的请求体与响应体,列出字段对应关系
  2. Ⅱ 在调用层集中写参数拼装函数
  3. Ⅲ 为超时和连接失败加分类重试
  4. Ⅳ 把错误返回按可重试与不可重试分组
  5. Ⅴ 用同一请求重复跑三次确认重试不产生重复写入
A A

把参数拼装、返回字段解析、超时与重试收进调用层,是判断「出参与入参对不上」的前提:业务代码只传语义参数、只读归一化结果,字段错位或重试写重了,排查范围能缩到一个文件里。建议的顺序是先抓一次真实收发记录,据此写字段对照表,再补集中拼装、错误分组和幂等键,最后用同一请求重复触发确认没有重复写入。

可落地的做法是先抓一次真实的请求体和响应体,把业务语义与两端字段名对齐,再把拼装、解析、超时、重试全部收进调用层;业务侧只传语义参数、只读归一化结果。重试只覆盖超时和连接类错误,确定性错误直接抛出。若官方文档没有写明字段名,不要凭记忆补齐,先用一次真实调用确认,再决定映射怎么写。

抓一次真实调用的请求体与响应体,列出字段对应关系

先看清实际收发的是什么,再讨论字段映射。记录方式按你能改动的层次选:一是打开所用 SDK 或客户端的调试日志开关,让它把请求体和响应体原文落到本地文件;二是用同样参数在命令行重放一次同一请求,完整保留响应头;三是在调用层临时加一行打印,输出请求体 JSON、响应头(重点看请求标识和内容类型)与响应体原文。打印时截断超长文本并脱敏,不要把关凭据写进日志。

字段对照表的列结构建议固定为七列:业务语义名、请求字段路径、响应字段路径、类型、是否必填、赋值来源、备注。一行只对应一个语义,不要把「输入内容」和「输出长度上限」挤在同一行。

业务语义名请求字段路径响应字段路径类型是否必填赋值来源备注
请求标识<幂等键请求头名>响应头中回显的请求标识string是调用层生成同一次逻辑请求的所有重试复用同一值
模型标识<模型标识字段>响应体中的模型名string是业务传入回显不一致时说明路由到了别的版本
输入内容<输入列表字段>—array是业务传入只做结构校验,不改写内容
输出文本—<输出文本字段>string是解析函数提取流式与非流式的取法可能不同
用量统计—<用量字段>object否解析函数提取缺失时不要报错,置空
错误信息—<错误码字段>object否解析函数提取用于下一节的错误分组

表中路径是占位写法,实际字段名以官方文档为准;文档没有写明的字段不要凭猜测补上,先看真实响应里到底返回了什么,再决定是否进入对照表。

在调用层集中写参数拼装函数

业务侧只传语义参数,拼装函数负责字段名映射、类型转换、空值填充;解析函数负责把响应原文转成固定结构。约定好之后,业务代码里不再出现任何接口字段名。

Ling-3.1-flash 的出参与入参对齐——超时重试写进调用层
# 通用接入骨架,字段名以官方文档为准
DEFAULTS = {'timeout_ms': 15000, 'max_attempts': 3}

def build_request(biz, idem_key):
    body = {}
    body['<模型标识字段>'] = biz['model']
    body['<输入列表字段>'] = biz['inputs']
    body['<输出上限字段>'] = biz.get('max_output', 512)
    headers = {
        'Content-Type': 'application/json',
        '<幂等键请求头名>': idem_key,
    }
    return {'body': body, 'headers': headers,
            'timeout_ms': DEFAULTS['timeout_ms']}

def parse_response(raw):
    return {
        'text': raw.get('<输出文本字段>'),
        'request_id': raw.get('<请求标识字段>'),
        'error': raw.get('error'),
    }

def make_idem_key(biz):
    # 由业务唯一键派生,可读且可复现
    return biz['tenant'] + '-' + biz['trace_id']

替换项有三处:字段名按官方文档填,默认值按业务上限填,超时值按客户端预算填。拼装函数里不要写业务判断,例如按用户等级改参数,那属于调用方要做的事,放进来会让这个函数重新变成第二个业务层。

为超时和连接失败加分类重试

重试的目标只是让偶发失败自动恢复,确定性错误不该在这里空转。判断分支按异常类型和状态码分开写,超时与连接失败才进入重试,其余直接抛出。

for attempt in range(1, max_attempts + 1):
    try:
        resp = call(build_request(biz, idem_key))
        if resp.status < 400:
            return parse_response(resp.body)
        cls = classify_http(resp)
        if not cls.retryable or attempt == max_attempts:
            raise CallError(cls, resp)
    except (Timeout, ConnectionReset, DnsError) as e:
        cls = classify_network(e)
        if attempt == max_attempts:
            raise CallError(cls, e)
    sleep(backoff(attempt))

重试间隔从配置读取,不要写死在代码里:给 initial_delay、multiplier、max_delay、max_attempts 四个值,间隔按 min(max_delay, initial × multiplier^attempt) 计算,再加一点随机抖动,避免多个请求在相近时刻同时重试。数值按你的接口响应时间和客户端等待预算调整,没有通用最优值。另外,已经开始返回内容的流式调用不要整段重试,否则会拼接出重复内容。

Ling-3.1-flash 的出参与入参对齐——超时重试写进调用层

把错误返回按可重试与不可重试分组

分组依据从两处读取:HTTP 状态码,以及响应体里可解析的错误码或错误类型字段。状态码覆盖超时、限流、服务端错误;响应体的业务错误码用来区分「服务端暂时不可用」和「请求本身有问题」。两者冲突时以业务错误码为准,并在日志里同时记录状态码和错误码,方便回看。

错误分类读取依据是否重试备注
客户端超时本地超时异常,无完整响应是重试前确认幂等键未变
连接失败、连接被重置、DNS 解析失败传输层异常是间隔可略长于超时类
HTTP 429状态码,可参考 Retry-After 响应头是优先按响应头给的时间等待
HTTP 5xx状态码通常是连续失败时由上层决定是否降级
HTTP 400、401、403、404状态码否请求或凭据问题,重试无用
参数校验失败、内容审核类错误响应体错误码或错误类型否需要改请求内容后才可能通过

分组结果记录在调用层返回的固定字段里,例如 error_class、retryable、attempts、request_id、idempotency_key。业务代码只看 retryable 和归一化数据,不自己判断状态码,这样同一类错误不会在业务层再被重试一次。

用同一请求重复跑三次确认重试不产生重复写入

幂等标识放在请求头里,或放在请求体中业务唯一键对应的字段里,具体哪个位置生效以官方文档为准。它在拼装函数中生成一次,写进这次逻辑请求的上下文,所有重试都复用同一个值;重试分支里绝不要重新生成,否则每次重试对服务端都是新请求。

检查方法:对同一逻辑请求连续触发三次,其中至少一次人为制造超时(把超时值调到很小,或指向一个不可达的地址触发连接失败)。然后核对三件事:调用层日志中三次的 idempotency_key 是否一致、attempts 是否按预期累加、失败后成功那次返回的资源标识是否与其他两次相同;再通过查询接口或对账 SQL 看目标记录是否只增加一条。若出现两条,说明幂等键没有透传或服务端不认这个键,需要换到文档支持的位置重新验证,而不是在业务层加去重逻辑绕过。