AnySearch 给 Agent 返回的是结构化结果,字段怎么取要先定

文章导读
AnySearch 这类检索接口返回给 Agent 的通常不是一段纯文本,而是带层级、带元信息的结构化对象。Agent 侧真正要做的是先拿到一次真实返回,把结构摊开,再按用途给字段分档,最后用白名单投影函数固定裁剪规则。全塞进上下文容易超长、挤掉指令空间;随手丢字段又会把判断依据丢掉,导致答得含糊。建议把“取哪些字段”当成配置项提前定下来,而不是每次调用时临时决定。
📋 目录
  1. A 用一次真实查询把返回体的层级摊开
  2. B 按 Agent 用途把字段分三档
  3. C 写字段投影函数只保留白名单
  4. D 用同一查询比对裁剪前后的占用
  5. E 字段缺失时的兜底取值顺序
A A

AnySearch 这类检索接口返回给 Agent 的通常不是一段纯文本,而是带层级、带元信息的结构化对象。Agent 侧真正要做的是先拿到一次真实返回,把结构摊开,再按用途给字段分档,最后用白名单投影函数固定裁剪规则。全塞进上下文容易超长、挤掉指令空间;随手丢字段又会把判断依据丢掉,导致答得含糊。建议把“取哪些字段”当成配置项提前定下来,而不是每次调用时临时决定。

处理结构化检索结果,先确认返回体的稳定字段和不稳定字段,再按“必进上下文 / 按需取用 / 直接丢弃”三档建白名单,用投影函数统一裁剪,用同一查询对比裁剪前后的文本长度与答题覆盖度。适用场景是 Agent 消费结构化搜索结果;边界是不能为了省长度删掉回答必需的原文字段,也不能把某次返回的偶发字段当稳定契约。

用一次真实查询把返回体的层级摊开

不要凭印象猜字段,先用一个能代表目标场景的真实查询跑一次,把原始 JSON 原样落盘,再逐层读。通常返回体顶层是状态码、结果数组、分页游标一类外壳字段,内容在数组元素里,元素内部又可能嵌着文本片段、高亮片段、来源信息、时间、评分等子对象。关注两层:顶层字段和嵌套字段各自的字段名与样例值。

记录时用一个简单表格或注释块即可,重点是标注“稳定出现”还是“偶发出现”。稳定出现的字段可以作为白名单默认项;偶发字段先按需取用,不要直接进默认投影。

# 调试用的临时记录,不进入生产代码
# 查询样例: query="示例问题"
# 顶层字段:
#   request_id      "abc-123"            偶发(部分返回带)
#   items           [ {...}, {...} ]     稳定
#   total           37                   稳定
# 嵌套在 items[*] 内:
#   title           "标题样例"           稳定
#   snippet         "正文片段样例"        稳定
#   url             "https://..."        稳定
#   published_at    "2025-xx-xx"        偶发
#   score           0.83                 偶发

做法上可以先用 jq 'keys' 看顶层,再对数组第一个元素执行 jq '.items[0] | keys',嵌套层级继续往下钻。同一查询多跑几次,把字段集合做交集,交集里的字段才算稳定字段。这一步不做完就定白名单,后面改配置的成本会明显变高。

按 Agent 用途把字段分三档

字段取舍要变成规则,规则依据是“Agent 回答这个问题时用不用得上”,不是字段大小或看起来重不重要。建议分三档,并写进配置文件的三个列表里,后续调整只动配置。

  • 必进上下文:没有它回答就无法落地,或者无法交代信息来源。判定标准是删掉之后回答会变空、变错或无法回溯。例子:title(判断结果是否切题)、snippet(回答正文依赖的原文片段)。
  • 按需取用:只有在特定问题类型下才需要,比如涉及时效或需要展示出处时。判定标准是默认不进,由查询意图或工具调用参数触发。例子:published_at(只做时效类提问时取)、url(只在需要引用来源时取)。
  • 直接丢弃:对回答没有增量信息,或属于传输层与调试层。判定标准是即使保留也不会改变回答内容。例子:request_id(排障用,不参与回答)、score(排序已由服务端完成,进上下文反而干扰模型判断)。

三档定完后,把“必进上下文”和默认的“按需取用”合并成投影白名单,其余一律不取。需要结合环境确认的是按需档的触发条件——不同 Agent 的问题分布不同,触发规则要按自己的场景调。

AnySearch 给 Agent 返回的是结构化结果,字段怎么取要先定

写字段投影函数只保留白名单

不要在每次拼提示词的地方手写取值路径,抽一个投影函数,输入原始结果和配置,输出裁剪后的对象。字段路径用占位符示意,接入时替换成实际返回结构里的键名即可。

# 白名单配置(占位键名,按实际返回结构替换)
PROJECTION = {
    "always":   ["title", "snippet"],
    "optional": ["published_at", "url"],
    "max_items": 5,
}

DEFAULTS = {
    "title": "",
    "snippet": "",
    "published_at": None,
    "url": None,
}

def project_item(raw_item, include_optional=False):
    fields = list(PROJECTION["always"])
    if include_optional:
        fields += PROJECTION["optional"]
    out = {}
    for key in fields:
        value = raw_item.get(key, DEFAULTS.get(key))
        # 空字符串和 None 都视为缺失,统一走兜底
        out[key] = value if value not in ("", None) else DEFAULTS.get(key)
    return out

def project_result(raw_result, include_optional=False):
    items = raw_result.get("items") or []
    return [project_item(i, include_optional)
            for i in items[:PROJECTION["max_items"]]]

这里有两个取舍点。一是 max_items:上下文长度受模型窗口限制,条数上限建议配成常量,而不是按返回长度动态算,便于复现。二是缺失默认值:文本字段给空字符串,链接和时间给 None,下游拼提示词时统一跳过空值,避免出现“标题:”后面什么都没有的残句。投影函数只做投影,不做判断,判断留给下一步。

用同一查询比对裁剪前后的占用

裁剪之后要证明没丢关键信息。做法是固定同一个查询,分别跑“原始返回直接拼接”和“经投影函数裁剪后拼接”两版,记录三项:拼出来的文本长度、条目数、回答能否覆盖原问题。文本长度用 len(text) 或字符统计即可,不需要精确到 token,看量级是否明显下降即可。

覆盖度靠人工读,不看数值:把两版回答并排,逐条对照原问题需要的要素——是否点到了核心结论、是否给出了可核查的出处或原文依据。如果裁剪后回答开始回避细节、只给模糊概括,说明 snippet 被截太短或白名单漏了字段,回到配置里补,不要改拼提示词的代码。这一步的结论只在你自己这个查询集上成立,换查询分布要重新比对。

字段缺失时的兜底取值顺序

单条结果里某个字段为空,不应该让整条作废,但也不能悄悄补一个看似合理的值。回退顺序建议按可靠性从高到低排:先取该字段本身,再取同一结果内的近义字段(例如摘要缺失时取正文片段字段),再取结果列表层面的共有信息(例如标题),最后才置空。

  1. snippet → 同类片段字段 → title → 空值并打标。
  2. published_at → 同结果内时间类字段 → 置 None 并打标,不做时间推断。
  3. url → 置 None 并打标,不要用搜索词拼一个假链接。

回退发生后要给该条结果打一个可信度标记,例如在投影结果里加 degraded: true 与缺失字段名列表。下游 Agent 看到标记,可以选择在回答里说明“该条摘要缺失”,或降低该条的引用权重。标记只作为提示,不要把它换算成具体分数,避免制造不可核验的量化结论。上线前用几条已知缺字段的历史返回回放一遍,确认标记确实被写出来。