一次调用先报错、随后拿到了答案,使用记录却出现两条:到底是网关重试,还是客户端又发了一次?如果只对照时间和模型名称,很容易把几条相似日志拼成一个并不存在的故事。
**先按请求标识分组,再看请求内部的尝试,最后核对最终结算。**同一个业务任务可能包含多个 HTTP 请求,一个 HTTP 请求又可能尝试多个渠道。请求数、尝试数和结算记录数需要分别解释。
本文给出一组可在本地复现的构造数据和查询方法。示例不连接真实模型,不使用客户日志,额度单位也不代表人民币或美元。目标是学会核对证据,不能用示例输出证明线上服务已经恢复。
1. 先给三种编号分工
| 标识 | 关联什么 | 不能代替什么 |
|---|---|---|
| 业务任务编号 | 用户希望完成的一次工作,例如一次文档总结 | 不能保证只有一次 API 请求 |
| 网关请求编号 | 网关接收的一次 HTTP 请求 | 不能直接视为上游厂商的请求编号 |
| 上游请求编号 | 某次上游交互返回的标识 | 不能单独覆盖所有重试渠道 |
再给网关内部的尝试增加序号,才能描述"同一请求的第一次尝试失败、第二次成功"。如果客户端断线后重新发起 HTTP 请求,新请求可能拿到新的网关标识;此时需要业务任务编号把两次请求联系起来。
请求编号用于关联。幂等机制则决定重复提交是否应该再次执行或结算,这是另一项业务约束。不能仅仅让两次请求带相同字符串,就宣布实现了幂等扣费。
本次核对的 New API 项目工作副本中,请求中间件会生成网关标识,写入 Gin 上下文和请求的 context,再设置 X-Oneapi-Request-Id 响应头。日志模型中分别存在 request_id 和 upstream_request_id 字段。这里描述的是所检查的实现,不保证每个部署版本、每个上游都返回完整标识。
2. 先用一组不会混淆的教学事件复现
下面代码保存为 trace_demo.py。demo-a、demo-b 和渠道别名都是人工构造标签;公开讨论真实事件时,不应贴完整请求标识、凭据或上游细节。
python
from decimal import Decimal
EVENTS = [
{'task': 'demo-task', 'request': 'demo-a', 'seq': 1, 'stage': 'accepted'},
{'task': 'demo-task', 'request': 'demo-a', 'seq': 2, 'stage': 'attempt', 'attempt': 1, 'route': 'route-A', 'status': 502},
{'task': 'demo-task', 'request': 'demo-a', 'seq': 3, 'stage': 'attempt', 'attempt': 2, 'route': 'route-B', 'status': 200},
{'task': 'demo-task', 'request': 'demo-a', 'seq': 4, 'stage': 'settled', 'net_quota': '120'},
{'task': 'demo-task', 'request': 'demo-b', 'seq': 1, 'stage': 'accepted'},
{'task': 'demo-task', 'request': 'demo-b', 'seq': 2, 'stage': 'attempt', 'attempt': 1, 'route': 'route-A', 'status': 200},
{'task': 'demo-task', 'request': 'demo-b', 'seq': 3, 'stage': 'settled', 'net_quota': '80'},
]
def trace(events, request):
selected = sorted(
(e for e in events if e['request'] == request),
key=lambda e: e['seq'],
)
attempts = [e for e in selected if e['stage'] == 'attempt']
settlements = [e for e in selected if e['stage'] == 'settled']
if len(settlements) != 1:
raise ValueError('最终结算缺失或重复,不能猜测扣费')
amount = Decimal(settlements[0]['net_quota'])
if not amount.is_finite() or amount < 0:
raise ValueError('教学净扣费字段非法')
return {
'request': request,
'attempts': len(attempts),
'routes': [e['route'] for e in attempts],
'net_quota': str(amount),
}
print(trace(EVENTS, 'demo-a'))
print(trace(EVENTS, 'demo-b'))
这个教学合同规定,每个请求必须恰好有一个最终净结算事件。因此缺失结算与重复结算都会报错,而不是默认按零计算。真实系统可能使用多条调整、退款或补偿流水,需要先将它们按实际账本规则对账,不能把这段单结算逻辑直接套到生产账本。
3. 看懂结果,再映射到自己的控制台
本地运行得到:
text
demo-a:2 次尝试,route-A → route-B,最终净额度 120
demo-b:1 次尝试,route-A,最终净额度 80
同一业务任务:2 个请求、3 次尝试、最终净额度 200
这说明"出现过一个 502"和"这个业务任务失败"不是同一件事。第一条请求最终经历了第二次尝试,但是否已经把完整答案交付给客户端,还需要客户端结果或流式结束证据;第二条请求是另外一次 HTTP 调用,不能被合并成第一次请求的渠道重试。
我参与 FishAI 的运营。读者可以从 https://yufish.cc/ 进入本人账号的使用记录,用发生时间、模型和请求标识核对自己的调用,再将实际可见字段映射到上述"请求---尝试---结算"三层。页面没有显示的渠道尝试或上游字段需要管理员核实,不能从两条相邻记录推断出来。本文不要求公开上传账号日志,也不把这组教学额度当作该站实测账单。
核对时先截取本人问题请求的小时间段,记录时区及原始字段。显示用的行号可能只是当前列表排序;它不适合作为跨页面、跨导出文件的关联键。找不到请求标识时,可以暂时用时间、模型、调用入口和结果缩小范围,但应该把最终关联标为"未确认"。
4. 给缺失和重复结算留出明确出口
把下面测试接到前面的代码末尾,执行 python3 trace_demo.py:
python
a = trace(EVENTS, 'demo-a')
b = trace(EVENTS, 'demo-b')
assert a['attempts'] == 2 and a['net_quota'] == '120'
assert b['attempts'] == 1 and b['net_quota'] == '80'
assert sum(Decimal(x['net_quota']) for x in [a, b]) == Decimal('200')
rejected = 0
for sample in [EVENTS[:-1], EVENTS + [EVENTS[-1]]]:
try:
trace(sample, 'demo-b')
except ValueError:
rejected += 1
assert rejected == 2
print('缺失与重复结算均已识别')
本地验收覆盖了两次渠道尝试与一个新请求的区分、最终额度求和,以及缺失和重复结算两个失败条件。这些是确定输入下的程序结果,不是成功率评测,也没有覆盖并发、网络、生产数据库或上游退款策略。
不要仅凭"余额减少了多少"复原一次请求。余额可能同时受其他请求、充值和调整影响;预扣值也不能直接当作最终净扣费。如果只能读到预扣事件,就应保留"最终结算未确认",等待同一请求的结算记录。
5. 常见断点要分别补证据
**响应头没有标识:**检查调用链是否经过了目标网关、代理是否保留该响应头,以及客户端是否能读取它。浏览器跨域访问还可能涉及响应头暴露设置。不要因为看不到头,就断言服务器没有生成编号。
**有请求编号,没有上游编号:**上游未返回、适配器未保存、错误在建立上游请求之前发生,都可能造成这个现象。没有对应证据时只写缺失,不填自造编号,也不把网关编号复制过去假装完整。
**一条请求有多个错误:**按尝试顺序核对原始分组、模型与渠道。最后一次错误、最终 HTTP 状态和用户收到的内容必须分别记录;完整路由信息应留在受权限保护的内部证据中。
**普通用户看不到审计字段:**权限过滤本身可能是设计行为。本次检查的工作副本会从普通用户日志的 other 中去掉 admin_info 和 audit_info。用户页面缺少这些字段,不等于管理员账本没有记录,更不是扩大客户可见权限的理由。
6. 最后交付一条能够核查的结论
一次排查报告至少说清:哪次请求、包含哪些已确认尝试、是否交付完整结果、最终结算是否对得上、还缺什么。可以写"第二次尝试返回成功,但流式结束与最终扣费仍未确认",不能把它缩成"已经恢复"。
对服务使用者,价值是知道应该提供哪几项信息;对网关维护者,价值是把日志、重试和账本串成可追溯的证据。编号只是起点,真正的验收是关联后没有把不同请求、不同权限和不同结算阶段混为一谈。
技术依据:2026-09-13 核对的工作副本中 middleware/request-id.go、common/constants.go、model/log.go;部署行为以当前版本实测为准。教学程序使用 Python 标准库,十进制计算参考 Python decimal 官方文档。本文由 AI 辅助整理,示例经过本地执行;未宣称客户案例或线上恢复结果。