先定查询粒度和返回字段——AnySearch 接入前要写清的三件事

文章导读
AnySearch 这类检索接口,真正花时间的部分往往在调通之后:结果太散或太粗,字段里一半用不上,超时以后不知道该怎么向调用方交代。要减少返工,接入前先把查询粒度、字段白名单、失败语义这三件事写成纸面约定,再动手写代码。查询粒度决定一次请求查一条还是查一批、top_k 给多大;字段白名单决定解析层要写多少个分支;失败语义决定空结果、超时、限流回来之后调用方怎么处理、要不要交给模型。这三件事一旦定
📋 目录
  1. 壹 把业务问题拆成最小查询单元
  2. 贰 用两三种粒度各跑一次做对照
  3. 叁 定字段白名单与必填校验
  4. 肆 定失败语义
  5. 伍 按清单逐项验证一遍
A A

AnySearch 这类检索接口,真正花时间的部分往往在调通之后:结果太散或太粗,字段里一半用不上,超时以后不知道该怎么向调用方交代。要减少返工,接入前先把查询粒度、字段白名单、失败语义这三件事写成纸面约定,再动手写代码。查询粒度决定一次请求查一条还是查一批、top_k 给多大;字段白名单决定解析层要写多少个分支;失败语义决定空结果、超时、限流回来之后调用方怎么处理、要不要交给模型。这三件事一旦定下,解析代码、提示词模板、日志字段都能一次写完,不用在联调中途回头重写。

适用场景:准备接入通用检索接口、又不确定一次查一条还是批量查的团队。操作动作:先用两三个真实业务问题写出最小查询语句,用粗、细两种粒度各跑一次做对照,再定字段白名单与必填校验规则,最后把空结果、超时、限流三类失败的返回结构和重试策略固定下来。验证方式:把每项约定变成一条可执行的检查,用日志或页面返回结果逐条核对。风险边界:具体接口支持哪些参数、是否支持批量、字段名如何拼写,都要以实际可用的配置和返回为准,本文给的是通用骨架,需要结合环境确认后再落地。

把业务问题拆成最小查询单元

不要从接口文档的参数出发,从用户真实会问的问题出发。挑两三个高频问题,逐个写成一条最小查询语句:能表达检索意图的最短字符串,加上必要的限定条件。写完之后再判断哪些可以合并成一次请求。

# 问题 1:退款多久到账
query: "退款 到账 时间"

# 问题 2:发票能不能重开
query: "发票 重开 条件"

# 问题 3:会员自动续费怎么关
query: "会员 自动续费 关闭"

可合并的情况通常有三类:几个问题共享同一检索域、同一 top_k、同一字段需求,且结果能按 query 原样回填区分;问题之间只是关键词替换,语义不依赖彼此;批量失败可以接受整体重试,不需要逐条控制超时。不可合并的情况也要写明:问题需要先改写再检索的(比如先要判断用户问的是账单还是功能)、需要逐条独立超时和降级的、以及多个问题共用一条结果会造成归属混乱的。批量请求看起来省事,代价是单条失败无法单独定位,日志里只能看到一批的结果,出问题时要重跑整批。

用两三种粒度各跑一次做对照

同一个问题,用粗粒度和细粒度各跑一次,把结果并排看。粗粒度一般指把用户原话整句丢进去,top_k 给大;细粒度指抽出核心关键词、必要时加域或时间限定,top_k 收小。判断依据不用复杂指标,就看两件事:返回条数是否超出调用方实际需要,前几条是否能直接回答问题。

先定查询粒度和返回字段——AnySearch 接入前要写清的三件事
对照项粗粒度(整句 + 大 top_k)细粒度(关键词 + 小 top_k)
查询语句退款多久才能到账啊退款 到账 时间
实际返回条数待填待填
前 3 条是否命中意图待填待填
是否混入无关内容待填待填
结论待填待填

假设调用方一屏只展示 10 条,粗粒度返回几十条并不会让回答更好,只会让解析层多做无用过滤;反过来,如果细粒度把限定加得太死、返回条数为零,就是粒度削过头了。选哪种取决于调用方的展示位和后续用途:需要给模型提供多样性证据时偏粗,需要直接命中一条明确答案时偏细。这张对照记录要留下来,后面调整参数时才有基准,不要只凭印象说“好像差不多”。

定字段白名单与必填校验

返回字段越多,解析层越容易写出用不上的分支。接入前把白名单列出来,只申请下面这些字段,其余一律不解析、不落库、不拼进提示词。

字段用途缺失时的处理
title结果展示与去重必填,缺失视为无效条目丢弃
url溯源与引用必填,缺失视为无效条目丢弃
snippet给模型做摘要输入降级为仅标题展示,不阻断
published_at时效判断标记为未知,时效敏感场景降低排序

必填校验的判定规则要能写成一行判断,不要含糊。建议:title 与 url 任一为空,该条目直接判无效;有效条目数少于调用方约定的最小条数时,整批结果标记为 degraded;snippet 缺失只降级不丢弃。校验失败后的动作固定为三步:先丢弃无效条目并计数,再判断剩余有效条目是否够用,不够则走兜底(换粒度重查一次,或用原始问题再查)。计数要打进日志,否则事后无法判断是接口返回变差还是本地解析写错。

def pick(item, fields=("title", "url", "snippet", "published_at")):
    return {k: item.get(k) for k in fields}

def valid(item):
    return bool(item.get("title")) and bool(item.get("url"))

定失败语义

空结果、超时、限流三类失败,返回结构最好统一成一个信封,调用方只判断一个字段就能分支。不要在调用方各处写 if 判断 HTTP 状态码,也不要让空结果和超时都表现为“没有内容”。

先定查询粒度和返回字段——AnySearch 接入前要写清的三件事
{
  "status": "ok | empty | timeout | rate_limited | error",
  "items": [],
  "retryable": false,
  "degraded": false,
  "message": ""
}
失败类型返回结构是否重试是否上报给模型
空结果status=empty,items 为空数组不重试,或换粒度重查一次上报“未检索到相关内容”,不要让模型自行补答案
超时status=timeout,retryable=true按约定次数重试,仍失败则降级上报检索不可用,模型应给出保守回复
限流status=rate_limited,带退避提示退避后重试,不立即重发上报服务繁忙,避免连续触发

三类失败的边界要说清:空结果不是错误,通常不重试,重试也大概率还是空;超时可能是瞬时也可能是查询本身太重,重试次数要有上限;限流的重试必须带退避,连续重发只会让情况更糟。每一种都要在提示词里留一句对应的话术,否则模型拿到空数组容易编内容。

按清单逐项验证一遍

约定写在文档里不算数,逐条跑一遍才算。下面这张清单可以直接照着做,状态栏按实际情况标“通过”或“待调整”。

检查项验证方式状态
最小查询语句能表达意图用真实问题跑一次,人工看前几条是否相关待填
批量与单条的边界已写清对不可合并的问题单独发一次请求,确认返回可区分待填
粒度已选定并留对照记录粗、细各跑一次,条数与相关性记入表待填
字段白名单与接口返回一致打印一条原始返回,逐个核对字段名待填
必填缺失能正确丢弃构造缺 title 或 url 的条目,确认被判无效并计数待填
三类失败返回结构正确分别制造空结果、超时、限流,看信封字段待填
降级路径可用强制触发降级,确认兜底查询被调用且有日志待填

验证时最好用同一组问题反复跑,记录改了哪一项、结果有什么变化。有些条目一跑就会发现要调整,比如字段名和接口实际返回不一致、批量请求里单条失败无法定位,这些都属于正常情况,改完再验一次即可。清单全部通过之后再合并代码,比联调中途反复改解析逻辑要省事得多。