让搜索 Agent 自己改进:从结果评估到迭代实验的工程闭环

2026-07-28 28 预计阅读时间: 1 分钟
来源: my.oschina.net AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:11 分钟

让 Agent 调用一次搜索接口并不难:生成查询词、发送请求、读取结果即可。真正棘手的是,当召回内容不相关、答案缺少证据或搜索成本过高时,Agent 能否判断问题出在哪里,提出改动,并通过多轮实验验证搜索是否真的变好了。

火山引擎开源 Agent 驱动的搜索自迭代技术,值得关注的核心正是这种能力边界的推进:搜索不再只是 Agent 的一个工具,而是一个可以被评估、调整和持续优化的子系统。由于来源摘要没有展开具体 API 和内部实现,下面不假设某个专有接口,而是从工程角度拆解一套可以实践的最小闭环。

一次搜索调用,与搜索自迭代的差别

普通搜索 Agent 往往是一条线性链路:

用户问题 -> LLM 生成查询 -> 搜索服务 -> LLM 汇总答案

搜索自迭代则需要在这条链路之外增加反馈回路:

任务集
  -> 当前搜索策略
  -> 执行搜索
  -> 评估结果
  -> 定位失败类型
  -> 生成候选策略
  -> 对照实验
  -> 保留更优版本

这里的“策略”不应只理解为 Prompt。它至少可能包含:

  • 查询改写:删除停用词、补充同义词、拆分复杂问题;
  • 搜索计划:先宽召回再精排,或者按时间、站点和文档类型分步搜索;
  • 工具参数:top_k、过滤条件、超时时间、重试次数;
  • 结果处理:去重、重排、证据片段抽取;
  • 停止条件:何时继续搜索,何时认为证据已经足够。

因此,自迭代的对象最好是一个结构化配置,而不是一段完全自由生成的文本。结构化配置更容易比较、回滚,也更适合进入持续集成流程。

评估器决定 Agent 会朝哪个方向优化

如果评价标准只是“最终回答看起来不错”,Agent 很容易学会写得更像正确答案,却没有改善搜索。一个可用的评估体系通常需要分层。

检索层指标关注目标证据有没有被找到:

  • Recall@K:正确文档是否出现在前 K 条结果中;
  • MRR:第一条正确结果出现得是否足够靠前;
  • 去重率与来源覆盖率;
  • 单次任务的查询次数、延迟和费用。

回答层指标关注搜索结果是否被正确使用:

  • 结论是否能由检索证据支持;
  • 引用是否与具体论断匹配;
  • 信息不足时是否明确表达不确定性;
  • 是否混入搜索结果之外的未经验证内容。

线上系统还应增加业务指标,例如问题解决率、用户追问率和人工接管率。优化目标可以写成一个带约束的分数:

总分 = 0.5 × 证据召回 + 0.3 × 答案可信度 + 0.2 × 来源覆盖
       - 延迟惩罚 - Token 成本惩罚 - 无效搜索惩罚

权重不是固定答案。面向故障排查的 Agent 可能更看重召回率,面向实时客服的 Agent 则必须同时约束延迟。关键是把目标写清楚,否则迭代过程只会把偶然波动当作改进。

一个可运行的最小自迭代实验

下面的示例不依赖外部服务,用本地文档模拟搜索引擎。它把查询扩展、停用词处理和 top_k 作为可变策略,在训练任务上选择候选配置,再用留出任务检查是否过拟合。

将以下内容保存为 self_iter_search.py,使用 Python 3.10 及以上版本运行。生产环境中可以把 search() 替换成真实搜索 API,把 mutate() 替换成 LLM 生成候选策略的步骤。

from __future__ import annotations

import re
from dataclasses import dataclass, replace

DOCS = [
    ('k8s', 'Diagnose a pod that keeps restarting with crash loop logs'),
    ('auth', 'Rotate an API credential safely without service downtime'),
    ('db', 'Speed up slow SQL queries by adding and validating indexes'),
    ('cache', 'Use a cache to reduce repeated network requests'),
]

TRAIN_TASKS = [
    ('pod keeps restarting', 'k8s'),
    ('renew service token', 'auth'),
]

HOLDOUT_TASKS = [
    ('reduce database query latency', 'db'),
]

STOP_WORDS = {'a', 'an', 'the', 'how', 'to', 'can', 'i'}
SYNONYMS = {
    'renew': ['rotate'],
    'service': ['api'],
    'token': ['credential'],
    'reduce': ['speed'],
    'database': ['sql'],
    'latency': ['slow'],
}


def tokens(text: str) -> list[str]:
    return re.findall(r'[a-z0-9]+', text.lower())


@dataclass(frozen=True)
class Strategy:
    drop_stop_words: bool = False
    expand_synonyms: bool = False
    top_k: int = 1


def rewrite(query: str, strategy: Strategy) -> list[str]:
    result = tokens(query)

    if strategy.drop_stop_words:
        result = [word for word in result if word not in STOP_WORDS]

    if strategy.expand_synonyms:
        expanded = list(result)
        for word in result:
            expanded.extend(SYNONYMS.get(word, []))
        result = expanded

    return result


def search(query: str, strategy: Strategy) -> list[str]:
    query_words = set(rewrite(query, strategy))
    ranked = []

    for doc_id, text in DOCS:
        doc_words = set(tokens(text))
        overlap = len(query_words & doc_words)
        ranked.append((overlap, doc_id))

    ranked.sort(key=lambda item: (-item[0], item[1]))
    return [doc_id for _, doc_id in ranked[:strategy.top_k]]


def evaluate(tasks, strategy: Strategy):
    failures = []
    hits = 0

    for query, expected_doc in tasks:
        results = search(query, strategy)
        if expected_doc in results:
            hits += 1
        else:
            failures.append({
                'query': query,
                'expected': expected_doc,
                'actual': results,
            })

    return hits / len(tasks), failures


def mutate(strategy: Strategy) -> set[Strategy]:
    candidates = {
        strategy,
        replace(strategy, drop_stop_words=not strategy.drop_stop_words),
        replace(strategy, expand_synonyms=not strategy.expand_synonyms),
        replace(strategy, top_k=min(3, strategy.top_k + 1)),
    }
    return candidates


def complexity_penalty(strategy: Strategy) -> tuple[int, int, int]:
    # 分数相同时,优先选择更少结果、更少改写的简单策略。
    return (
        strategy.top_k,
        int(strategy.expand_synonyms),
        int(strategy.drop_stop_words),
    )


def main() -> None:
    current = Strategy()

    for round_no in range(1, 4):
        candidates = mutate(current)
        scored = []

        for candidate in candidates:
            score, failures = evaluate(TRAIN_TASKS, candidate)
            scored.append((score, candidate, failures))

        scored.sort(
            key=lambda item: (-item[0], complexity_penalty(item[1]))
        )
        best_score, current, failures = scored[0]

        print(f'round={round_no} train_score={best_score:.2f}')
        print(f'strategy={current}')
        print(f'failures={failures}\n')

    holdout_score, holdout_failures = evaluate(HOLDOUT_TASKS, current)
    print(f'holdout_score={holdout_score:.2f}')
    print(f'holdout_failures={holdout_failures}')


if __name__ == '__main__':
    main()

运行命令:

python self_iter_search.py

这个示例刻意保持简单,但已经包含自迭代系统的几个关键部件:可修改的策略、固定任务集、自动评估、候选实验、复杂度约束和留出集验证。接入 LLM 后,可以让模型读取失败样本并输出结构化修改建议,例如:

{
  "failure_type": "vocabulary_mismatch",
  "changes": {
    "expand_synonyms": true,
    "top_k": 2
  },
  "reason": "The query uses token while the target document uses credential."
}

不要让模型直接修改线上策略。更稳妥的做法是:模型只生成候选配置,实验执行器负责校验字段、限制参数范围,并在离线数据或影子流量上比较新旧版本。

从演示走向生产,需要补上的护栏

搜索自迭代并不等于无限循环。没有边界的优化过程可能产生四类问题。

评估器投机。 Agent 可能迎合评分器,而不是改善真实搜索。应混合使用确定性指标、人工标注和盲测,并定期检查高分失败案例。

训练集过拟合。 某个改写规则可能只对少量问题有效。任务集应按时间、领域和难度分层,保留从未参与策略选择的测试集。

成本失控。 提高 top_k、增加查询分支通常能换来更高召回,但也会增加搜索费用和延迟。每轮实验都应记录查询数、Token、耗时和外部 API 成本。

策略污染与安全风险。 搜索页面可能包含提示注入内容。系统应把网页文本视为不可信数据,禁止搜索结果直接改变系统指令、凭据权限或发布流程。

一套务实的上线检查表可以是:

  • 策略配置是否版本化,并能一键回滚;
  • 每次改动是否记录任务集、指标、成本和失败样本;
  • 是否同时使用训练集、验证集和长期保留集;
  • 候选策略是否经过参数校验和权限限制;
  • 新版本是否先运行离线评测,再进入影子流量或小比例灰度;
  • 指标下降、成本突增或异常查询出现时,是否自动停止迭代。

搜索 Agent 的下一阶段,不是“再接一个搜索工具”,而是把搜索策略本身变成可观察、可实验、可回滚的软件资产。先建立可靠评估,再开放自动修改;先让 Agent 提议实验,再逐步扩大它的执行权限。这样的自迭代才可能带来稳定收益,而不是制造一个更昂贵、更难解释的搜索循环。


相关推荐