两个入口答案不一致先别调模型:6 行入口口径对账表(附脚本)

两个入口答案不一致先别调模型:6 行入口口径对账表(附脚本)

企业知识库上线之后,有一类反馈很稳定:用户说系统答错了,发来截图;把那句话原样拿到网页端问一遍,答案是对的。

同一份索引,同一个模型,同一个检索服务。于是排查方向通常直接指向模型,开始调温度、换模型、加 prompt。我在这条路上浪费过时间,现在把顺序反过来:先把两个入口实际提交的东西对一遍,对不上就先修入口。

一、6 行入口口径对账表(接口端视角)

下面这张表是我现在用的对账口径。填表时站在接口端看,症状列写的是线上会露出什么痕迹,方便直接从日志反查。

对账项 网页端 接口端 线上露出的痕迹
query 原文 经历史对话改写 用户原话 接口端日志里的 query 短、无主语
hist_turns 带最近若干轮 一般为 0 多轮会话第二轮起答案明显变浅
top_k 较大 偏小 引用来源数量少于网页端
filters 带业务线与时间范围 常为空 召回结果里混进其它产品线文档
rerank 开启 常关闭 命中段落排序与网页端不一致
index_version 最新切片 可能指向旧切片 新增文档在接口端查不到

这张表的用法是「填一遍」,不是「看一眼」。示例 json 只演示格式,真正排查时必须取运行时打印出来的值,配置文件的默认值和口头描述都不算数。

二、为什么这六行值得单独对

query 与 hist_turns 出问题的概率最高。 网页端为了召回好,常会用历史对话改写当前问题;接口端一般不改写,也不带历史。这两项一缺,接口端提交的就是一句没头没尾的短句。

top_k、filters、rerank 属于参数漂移。 这三处通常是两个团队各改各的,改的人并不知道另一边还有一份配置。

index_version 最容易被忽略。 索引版本号一般不进业务日志,等答案不对想起来查,会发现接口端指向的还是一份旧切片。

三、用脚本对,不用人眼对

人眼对这六行,尤其是在两个环境之间来回切的时候,几乎必然出错。我写了一个很短的对账脚本,把两边的参数快照存成 json,跑一次直接出结果。

python 复制代码
"""入口口径对账:比较两个入口提交的检索参数快照。

用法:python check_entry.py web.json api.json
退出码:0 = 六个键齐全且值一致;1 = 存在不一致行;2 = 用法错误;3 = 字段缺失或类型不符。
"""

import json
import sys

# 对账顺序即排查顺序,不要调整
ROWS = ["query", "hist_turns", "top_k", "filters", "rerank", "index_version"]

# 这些键必须是指定的类型,否则 True 与 1 会被判成一致
TYPES = {"rerank": bool, "top_k": int, "hist_turns": int, "query": str, "index_version": str}


def norm(v):
    """归一化:filters 排序,其余原样,避免顺序差异被误判为不一致。"""
    if isinstance(v, list):
        return sorted(v)
    return v


def validate(name, snap):
    """返回 (缺失键列表, 类型不符键列表)。字段缺失必须单独报警,
    否则两个文件同时少一个键会被判成「一致」。"""
    missing = [k for k in ROWS if k not in snap]
    badtype = []
    for k, want in TYPES.items():
        if k in snap and not isinstance(snap[k], want):
            badtype.append(k)
    return missing, badtype


def show(v):
    """列表用逗号拼接展示,避免长列表把列撑破。"""
    if isinstance(v, list):
        return ",".join(str(i) for i in v) or "(空)"
    return str(v)


def diff(a, b):
    out = []
    for k in ROWS:
        va, vb = norm(a.get(k)), norm(b.get(k))
        if va != vb:
            out.append((k, va, vb))
    return out


def main():
    if len(sys.argv) != 3:
        print("usage: python check_entry.py web.json api.json")
        return 2

    with open(sys.argv[1], encoding="utf-8") as f:
        web = json.load(f)
    with open(sys.argv[2], encoding="utf-8") as f:
        api = json.load(f)

    hit = False
    for name, snap in (("web", web), ("api", api)):
        missing, badtype = validate(name, snap)
        if missing:
            print(f"MISSING {name}: {missing}")
            hit = True
        if badtype:
            print(f"BADTYPE {name}: {badtype}")
            hit = True
    if hit:
        return 3

    rows = diff(web, api)
    if not rows:
        print("OK 六行口径一致")
        return 0

    print(f"{'对账项':<16}{'网页端':<20}{'接口端'}")
    for k, va, vb in rows:
        print(f"{k:<16}{show(va):<20}{show(vb)}")
    print(f"diff_count={len(rows)}")
    return 1


if __name__ == "__main__":
    sys.exit(main())

下面是示例数据的实机输出。两份 json 内容如下(仅为演示格式):

json 复制代码
{"query":"退款申请提交后多久到账","hist_turns":3,"top_k":8,"filters":["biz=order","year=2026"],"rerank":true,"index_version":"v12"}
{"query":"退款多久到账","hist_turns":0,"top_k":3,"filters":[],"rerank":false,"index_version":"v11"}

跑一遍:

text 复制代码
对账项             网页端                 接口端
query           退款申请提交后多久到账         退款多久到账
hist_turns      3                   0
top_k           8                   3
filters         biz=order,year=2026 (空)
rerank          True                False
index_version   v12                 v11
diff_count=6

退出码是 1。另外两种退出码也要用上:字段缺失返回 3,用法错误返回 2。字段缺失单独报警这件事很关键,否则有人改了快照结构少打一个字段,两边同时缺这一项,脚本会判成一致,CI 就静默失效了。

四、对完之后的三件事

第一件:把前置处理收进主链路。 query 改写、上下文拼接这些动作放在前端或 BFF 里,就一定会出现两份实现。移进主链路,两个入口调同一个函数,从根上消除分叉。

第二件:可选参数改成显式必填。 history 允许传空数组,但必须传。显式传空和忘了传,在日志里要能区分开,否则排查时无法判断是调用方的决定还是漏传。

第三件:降级必须带标记。 接口端超时走了兜底返回短答案,返回体里要带降级标记。没有这个标记,外面看到的是答案错了,链路里发生的是根本没走到检索,两条路查下去成本差很多。

五、什么时候才该去查模型

六行全部对上,答案还是不一致,这时才轮到模型和索引。

需要说明一句:这六行管的是入口侧参数,超时降级不在其中,要看返回体的降级标记,别以为对完六行就覆盖了全部情况。

这个顺序的意义不只是省时间。参数快照本身就是一份回归基线,下次有人改了前端的改写逻辑,跑一次脚本就能发现,不用等客户发截图。

把表存下来,下次遇到同样的问题,先填一遍再下结论。

你那边最近一次「两个入口答案不一样」,最后查出来是哪一行?

相关推荐
Yang96111 小时前
0.8米超短盲区,解析BM-T1如何服务矿石能源光缆测试
1024程序员节
BK贝壳1 小时前
bool-hybrid-array 落盘我只认 .bha:用 Create_BHA / Ask_BHA 做布尔数组序列化
数据结构·python·numpy·1024程序员节
武天旭1 小时前
AI安全--AI大模型的隐私保护与法律法规
人工智能·安全
by209991 小时前
Linux 基础指令与文件操作
linux·1024程序员节
老徐聊GEO1 小时前
长尾问法召回对比实验(2026):全国大词与“南京+行业“问法的差异测量
1024程序员节·geo优化
优橙教育1 小时前
AI Agent开发是什么,和接个聊天框有什么区别
1024程序员节
Solange__2 小时前
影视预演怎么用AI生成3D场景?从分镜、Blockout到镜头交付的完整流程
人工智能·游戏·3d·ai·自然语言处理·aigc·游戏策划
宝绿特塑业再生科技2 小时前
改性HDPE再生颗粒在户外家具挤出发泡板材中的色差控制与高品质价值重构
经验分享·科技·学习·生活·1024程序员节·户外家具
Sakura_j_h_2 小时前
Python 在定时任务里输出 emoji,UnicodeEncodeError 从哪来
开发语言·字符编码·windows·python·定时任务