尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

联邦搜索在AI Agent中的架构设计与工程落地

联邦搜索在AI Agent中的架构设计与工程落地 联邦搜索Federated Search是面向多个独立数据源做统一检索的一种架构最近在 AI Agent 场景里越来越常见。它解决的核心问题很明确当 Agent 要回答一个问题时往往需要同时查知识库、内部文档、工单系统、业务数据库甚至外部网页如果每个来源都单独接一套接口、单独处理返回格式、单独排序开发量就会快速增长检索质量也很难控住。这篇内容主要写给正在做 Agent 搜索链路、RAG 多路召回、企业内部知识聚合的开发者。我先把结论放在前面联邦搜索在 AI Agent 中的价值不是“多接几个搜索来源”而是用一套统一入口把多源查询、结果融合、权限控制这三件事同时管起来。如果这三件事没有理清接再多的数据源也只是增加维护成本甚至会把错误结果、重复结果和越权内容送进 Agent 的上下文。下面按实际落地顺序拆解先讲清楚它和普通搜索、RAG 检索的区别再拆组件、给最小实现思路然后重点说结果合并、权限安全、批量化和常见排查。过程中会带一些参数建议和判断标准方便你照着验证。1. 为什么 AI Agent 需要联邦搜索而不是直接连 N 个数据源1.1 Agent 多源检索的典型痛点先想象一个真实场景。企业内部部署了一个 AI 助手用户提问“新版支付服务最近为什么老超时”。这个问题的答案分散在多个系统里故障记录在工单系统接口文档在内部 Wiki调用链路的日志在日志平台可能还有一段最近的代码变更记录在代码库。Agent 如果想要回答完整就必须同时去这几个地方检索。如果每个数据源都单独写一个查询逻辑Agent 或者工具层的代码就会变成这样先调 Wiki 接口解析返回的 JSON再调工单接口处理另一套字段再调日志平台可能有自己的查询语法。每多接一个来源都要做一遍“鉴权、查询、解析、错误处理、结果格式化”。时间长了连接逻辑和业务逻辑混在一起改一个数据源的字段名都可能影响整个链路。更麻烦的是结果合并。不同来源返回的排序方式完全不一样Wiki 可能按相关度返回工单系统可能按创建时间排序代码库可能按文件路径匹配。如果不做统一处理Agent 拿到的是顺序混乱、字段不一致、还大量重复的信息很难基于这些内容给出稳定回答。还有一个容易出问题的地方是权限。搜索系统只负责返回结果不关心当前用户能不能看这些内容。如果 Agent 的账号权限过大或者连接器用了高权限凭据敏感文档就会通过检索结果进入 Agent 上下文再被拼进回答。这种问题在普通搜索里只是权限管理问题在 Agent 场景里会变成直接的信息泄露。1.2 联邦搜索解决的三类核心问题联邦搜索的定位就是在“多个数据源”和“Agent 使用层”之间加一个统一检索层。它不是为了替代已有的搜索引擎而是把不同的查询能力收拢到同一个入口。落到实际它主要解决三类问题。第一是统一入口。Agent 只需要向联邦搜索服务发起一个查询服务端按照配置把请求广播到多个连接器最后汇总返回。对 Agent 来说它不需要知道数据存在 MySQL 还是 Elasticsearch也不关心底层是 API 还是数据库直连。第二是结果融合。联邦搜索会把各来源返回的结果转换成统一结构再做去重、得分归一化、排序和截断。这样 Agent 拿到的不是一堆杂乱片段而是一份相对整齐、有序的候选列表。第三是权限控制。搜索服务可以在连接器层和结果返回层都做权限过滤保证不同用户问同样的问题能看到的内容范围不同。这一点在 Agent 场景里尤其重要因为 Agent 会自动跨系统取数人工逐个检查结果不现实。1.3 和普通搜索、RAG 检索的差别很多人会把联邦搜索和“多路召回”“RAG 检索”混在一起其实它们解决的是不同层面的问题。普通搜索通常面向单个索引或单个系统比如一个站内搜索框只查自己的文档库。RAG 中的向量检索一般也是从一个向量库召回 top-k 文档再用重排模型做精排本质还是在少数几个索引里做搜索。联邦搜索强调的是“联邦”两个字数据源彼此独立可能使用不同的协议、不同的数据模型、不同的访问权限。联邦搜索负责把这些异构系统组合成一个虚拟的统一搜索入口。它不强制每个源都变成向量库也不要求所有源使用同一种排序规则。三类检索在实际架构中也可以叠加先用联邦搜索把多个数据源的结果聚合起来再作为 RAG 的外部工具帮大模型补充上下文。所以它不是替代关系而是更靠前的检索编排层。2. 联邦搜索的核心组件与一次查询的完整流程2.1 核心组件拆解要做一套能用的联邦搜索不需要一开始就追求很重的中间件。只需要把下面几个组件划分清楚查询接收器接收 Agent 传来的 query、用户身份、过滤条件。连接器管理维护每个数据源的连接信息、认证方式、查询接口、超时设置。执行引擎把查询并发分发到多个连接器并处理部分失败。结果标准化把不同来源的返回内容转换成统一结构。合并排序器负责去重、得分归一化、融合排序和 top-k 截断。权限过滤器基于用户身份过滤不可见内容。缓存模块对高频查询做短时间缓存降低下游压力。日志与监控记录每次查询的耗时、来源数、返回量、错误率。组件数量看起来多但在小规模实现里很多可以用函数和配置代替。比如连接器管理可以是一张配置表执行引擎可以是一个并发循环结果标准化可以是一组转换函数。核心不在于用了什么框架而在于职责边界是否清晰。2.2 一次查询是怎么跑完的考虑一个标准的执行流程。Agent 发送一条查询联邦搜索服务先做查询解析提取关键词、实体、筛选条件并且带上用户身份信息。接着执行引擎会查看配置确定这次查询要覆盖哪些数据源并为每个源准备一个查询任务。这些任务一般是并发执行的。每个连接器对应一个数据源适配器它负责把统一查询请求转换成目标系统能理解的查询。比如对数据库连接器会拼出一条带 where 条件的 SQL对内部 Wiki 连接器会调用它的搜索 API对网页搜索连接器则调用外部搜索接口。各连接器返回结果后执行引擎不会立刻把这些结果拼给 Agent。它先做标准化把“标题、内容、链接、来源、得分、更新时间、权限标记”这些字段统一然后进入合并排序阶段。这个阶段处理重复项、修正不同源的分数可比性、按照融合规则重新排序最后截取 top-k。权限过滤会在排序后或排序前做一次具体看架构选择但必须在返回给 Agent 之前完成。2.3 统一数据模型返回给 Agent 之前先归拢落地联邦搜索时我建议第一步就定义统一结果模型不要等接了三个源以后再补。常见字段可以包括字段说明result_id全局唯一 ID用于去重和缓存title结果标题snippet摘要或正文片段url原始链接或内部地址source来源名称如 wiki、ticket、codescore标准化后的相关度分数permission最低可见权限标记updated_at内容更新时间raw原始结果用于调试和扩展不同系统返回的字段可能差很多但统一模型只需要保留 Agent 真正需要的字段以及排序去重需要的字段。多余内容先放进 raw不要直接参与主流程。这样后续加新数据源时只需要为它写一个转换器把字段映射到统一模型就能接入。2.4 连接器怎么适配不同数据源连接器是联邦搜索里工作量最大的部分。每个数据源都像一个方言不同的系统连接器就是一个翻译层。常见的数据源类型有结构化数据库、全文搜索引擎、向量数据库、内部 API、文件存储、外部网页搜索。适配这些数据源通常有两种方式对支持查询 API 的系统连接器直接封装 API对没有现成查询接口的数据源需要先建立索引或者通过离线同步把它导入搜索引擎再走搜索查询。连接器需要重点处理三件事认证鉴权、查询转换、错误处理。认证鉴权要保证无论底层系统用什么方式登录连接器都只使用最小必要权限。查询转换要做到把统一查询映射成源系统语法比如把“支付服务超时”转成数据库里的模糊匹配条件转成 Elasticsearch 的 match query。错误处理要区分超时、鉴权失败、限流、数据格式异常并给上层返回可识别的原因。3. 最小可运行实现先单源再聚合3.1 建议的落地顺序很多人第一次实现联邦搜索时喜欢把所有数据源一次性接进来。我不建议这么干。联邦搜索的调试难度会随着数据源数量线性增长而排查问题的复杂度则增长得更快。更稳妥的顺序是先跑通一个数据源确认查询、返回、标准化都没问题再加第二个数据源这时候重点观察合并排序和去重是否正确最后再把并发、超时、权限过滤加进来。这个顺序能让每一层的变量都尽量少出了问题更容易定位。我一般会先在本地写一个最小脚本用一个测试查询验证两条链路单源返回有没有内容多源合并结果是否合理。跑通之后再考虑要不要封装成 HTTP 服务供 Agent 调用。3.2 环境与前置条件最小实现不需要特别重的环境。常见选择是 Python 或 JavaPython 更适合快速验证Java/Spring 更适合后续做服务化。前置条件有四个能访问至少一个数据源比如一个测试数据库、一个内部搜索 API或者一个本地文件索引准备好认证凭据并确认它只有只读权限确定统一的返回结构可以先按第 2.3 节里的字段定义准备一个测试查询集合不要只是一条至少要有相关查询、无结果查询、容易被多个源同时命中的查询。硬件方面本地验证阶段不需要 GPU内存 8GB 以上就能跑。如果数据源全是远程 API主要瓶颈在网络和 API 的限流如果本地还要建索引才需要关注磁盘和内存。3.3 一个最小示例的代码思路这里给一个示意性的代码结构不是完整生产实现。它的作用是展示职责拆分。import logging from concurrent.futures import ThreadPoolExecutor def federated_search(query, sources, user_ctx, top_k10): raw_results [] with ThreadPoolExecutor(max_workerslen(sources)) as executor: future_map { executor.submit(connector.search, query, user_ctx): name for name, connector in sources.items() } for future, name in future_map.items(): try: items future.result(timeout5) raw_results.extend(normalize(name, items)) except Exception as exc: logging.warning(%s search failed: %s, name, exc) merged dedup(raw_results) ranked rank(merged, query) filtered filter_by_permission(ranked, user_ctx) return filtered[:top_k]# 统一结果标准化示意 def normalize(source_name, raw_items): normalized [] for item in raw_items: normalized.append({ result_id: f{source_name}:{item.get(id)}, title: item.get(title, ), snippet: item.get(snippet, item.get(content, )), url: item.get(url, ), source: source_name, score: float(item.get(score, 0)), permission: item.get(permission, ), updated_at: item.get(updated_at, ), raw: item, }) return normalized这段代码里每个连接器只负责返回自己的结果统一转换发生在 normalize 里去重、排序、权限过滤都是独立的函数。这种结构的好处是后续新增数据源时尽量少改动主流程。3.4 第一步跑通后要看的输出和日志第一次跑通不等于结束。要确认三件事结果数量是否合理每个源是否都有返回返回的内容有没有明显错误。我建议在日志里至少打印这些信息query、来源数量、每个来源的返回条数、整体耗时、超时或失败的来源名称、最终返回的 top-k 数量。这些信息在后续排查时非常关键。如果某些源返回为空先不要急着调排序。先确认是源系统里真的没有匹配内容还是查询语法没转换对还是鉴权失败。看日志比改参数更靠前。通过之后可以把状态整理成一份简单的接口文档记录每个数据源支持什么查询方式、典型耗时、容易失败的场景。这份文档以后会救你很多次。4. 结果合并、去重与排序的工程细节4.1 不同来源的分数能不能直接比较不能。不同系统给出的 score 几乎没有可比性。有的系统返回相关度百分比有的返回整数匹配数有的甚至不返回分数。如果直接把分数相加或者取最大结果大概率是某个特殊分数体系主导了整个排序。常见的处理方式有三种。一是排名倒推把每个源内部的排序序号作为基础第一名的权重最高不关心原始分数。二是做归一化把每个源的分数缩放到 0 到 1 之间比如除以该源返回的最大分。三是用百分位把每个源的分数映射到分位数消除不同分布的影响。具体选哪种取决于你对数据源的信任程度。如果一个源本身排序质量就不稳定直接用原始分数会放大噪声这时用排名倒推更稳。4.2 去重策略多源检索一定会碰到重复结果。同一个文档既在 Wiki 里有副本也在共享文档库里有副本内容一致但链接不同。如果不做去重Agent 上下文里会出现大量冗余内容既增加 token 消耗又可能干扰回答。去重不能只靠标题完全一致。建议用三层判断第一层看 result_id 里的来源前缀和原始 ID适合识别完全相同的条目第二层看 URL 规范化后的链接适合识别同源不同 ID 的重复第三层看标题和摘要的归一化哈希适合识别跨源重复。去重的基本原则是保留来自权威源的那条而不是任意保留。可以在配置里给每个数据源设定优先级比如内部 Wiki 高于网页搜索结果。这样用户看到的链接更可控也能减少后续打开错误链接的情况。4.3 融合排序怎么做去重之后需要把来自不同源的结果重新排序。最简单的做法是加权求和每个源一个权重把归一化后的分数乘以权重再相加。这个方案容易理解也容易调参。如果效果不够好可以在联邦搜索之后再加一个重排层。重排层可以使用更复杂的信息比如查询和结果片段的语义相似度、结果权威性、时效性、权限层级等。对 Agent 场景来说这一步很值得做因为 Agent 对上下文质量比普通搜索用户更敏感。重点提醒不要一上来就调特别复杂的排序模型。先用小样本看结果顺序是否合理问题主要出在某个源返回质量差、还是重复没清干净、还是权重不合理。很多时候是前置处理没做好而不是排序策略不够高级。4.4 缓存与超时设置Agent 的查询往往具有重复性比如多轮对话中多次提到同一个主题。短期缓存能明显降低下游系统压力也能让响应更快。缓存 key 建议包含query 规范化后的内容、用户所属权限组、要检索的数据源列表、必要的过滤条件。缓存过期时间不宜太长10 到 60 秒比较常见。如果是实时性要求高的日志或工单数据建议缓存时间更短或者直接不缓存。超时设置也很关键。每个连接器的超时时间不能比整体超时长。例如 Agent 端期望 3 秒内返回整体服务最多留 2.5 秒再往前推单个连接器的超时可能只有 1.5 到 2 秒。超时后要记录来源名称并让其他源的结果继续返回而不是等待所有源都完成。5. 权限、合规与安全边界不能到 Agent 这层才处理5.1 为什么权限过滤要前置到连接器Agent 的出现让权限问题从“查询时限制”变成了“自动跨系统取数时如何限制”。如果只在最终回答层过滤风险很高因为 Agent 可能已经通过检索结果间接接触到了敏感内容。更安全的做法是把权限过滤前置到连接器。连接器发起查询时就应该携带用户身份信息只调用该用户有权访问的数据范围。比如在数据库连接器里查询条件必须带上当前用户可见的租户 ID在工单连接器里只能查用户所属团队或已授权分组的工单在 Wiki 连接器里只搜索用户有权限的空间。这样做的原因是底层数据源往往有最精确的权限边界比上层统一模型更可靠。联邦搜索层很难完整复制所有数据源的权限规则所以更稳的做法是“能下推就下推”。5.2 权限控制的最小实现思路最小实现至少要做两层过滤。第一层是连接器查询时限制。每个连接器在构造查询时使用 user_ctx 中的用户 ID、角色、权限组限制搜索范围。已经支持 ACL 的系统直接把权限参数传进去不支持的连接器要在查询语句里加权限条件。第二层是结果返回前过滤。有些数据源的权限是粗粒度的返回结果里可能混入了不可见内容。联邦搜索服务在合并排序后要根据统一结果模型里的 permission 字段再做一次过滤。两层都做才能最大程度避免越权。如果权限判断结果不确定我建议宁可不返回该条结果也不要冒险暴露。联邦搜索场景下少一条搜索结果通常只是检索不完整多一条不该出现的结果可能就是安全事故。5.3 日志审计与合规留痕因为联邦搜索会跨系统取数所以审计日志比单库搜索更需要重视。每次查询至少要记录查询用户、请求时间、涉及的数据源、每个源返回数量、最终返回结果 ID 列表、是否触发权限过滤、耗时。这些日志一方面用于排查问题另一方面也是合规留痕。如果后续被问“某个用户通过 Agent 查到了哪些内容”有完整的审计日志才能回答。日志里需要注意不要记录完整敏感内容特别是用户私有信息。一般记录标题、摘要片段和权限标记就够用完整内容存在源系统本地。5.4 输入与输出的双重边界除了权限还要防止查询输入把联邦搜索变成不安全的入口。输入层面要对 query 做注入防护。比如连接器在拼 SQL 或查询语法时必须使用参数化方式不能直接把用户输入拼进查询语句。越权查询、命令注入、恶意语法都需要在统一查询解析层就过滤掉。输出层面要对返回结果做合规过滤。包含敏感个人信息的结果最好不要原样进入 Agent 上下文。可以在返回前做脱敏处理比如隐藏邮箱、手机号、密码等字段。脱敏逻辑要放在权限过滤之后、返回给 Agent 之前。这种双重边界不仅是技术问题也是责任边界。联邦搜索层要明确哪些控制由数据源负责哪些由搜索层负责。边界清晰才不会出现互相推诿的安全漏洞。6. 批量任务、接口化与生产化落地6.1 从单条查询到批量查询Agent 场景里不总是一问一答一些任务会批量触发检索。例如批量处理用户反馈时需要一次性对几百条问题做查询再交给大模型分析。单条查询能跑通不代表批量也能直接跑。批量场景要额外处理四件事队列、限流、并发、失败重试。队列可以让请求不至于瞬间压垮下游系统。限流要根据各个数据源的 API 配额来设置不能一个源允许每秒 100 次另一个只允许 10 次用同一个并发数去跑。并发数的调整原则是先小后大观察错误率再逐步增加。失败重试要设置最大次数一般两到三次就够重试之间要有间隔。批量查询的结果要能追踪批次和单条。建议给每个批次生成 batch_id每条查询关联 query_id输出的结果文件或消息里带上这些标识。否则批量任务失败时很难定位是哪条查询出了问题。6.2 Agent 调用层的接口设计建议如果要做成服务接口建议设计得尽量贴近 Agent 的调用习惯而不是暴露底层连接器细节。一个请求可以包含query、user_context、sources可选、top_k、filters、timeout。一个响应可以包含results、total、query_id、source_stats、warning。source_stats 里可以记录每个来源是否成功、返回多少条、耗时多少。这些信息对 Agent 判断是否继续追问很有帮助。{ query: 支付服务超时, sources: [wiki, ticket, code], top_k: 10, user_context: { user_id: u_1001, permission_groups: [payment_team, internal_wiki] }, filters: { created_after: 2025-01-01 } }{ query_id: q_20251025_001, total: 8, results: [ { result_id: wiki:12345, title: 支付服务超时排查手册, snippet: 当支付服务响应时间超过 3 秒时..., url: https://internal.example.com/wiki/12345, source: wiki, score: 0.92, updated_at: 2025-10-20 } ], source_stats: { wiki: {status: ok, count: 5, time_ms: 120}, ticket: {status: failed, count: 0, time_ms: 3000} } }接口层不建议把原始连接器错误直接抛给 Agent。原因是连接器错误往往包含内部信息对 Agent 没有帮助还可能泄露系统结构。更合适的做法是把错误归类为超时、鉴权失败、限流、无结果再通过 source_stats 返回。6.3 性能监控与排查链路生产环境需要监控几个核心指标单次查询整体 P50/P95 耗时、每个数据源的平均耗时和错误率、重复结果比例、权限过滤触发次数、缓存命中率、最终 top-k 返回条数。这些指标能帮助判断性能瓶颈在哪里。如果整体耗时过高先看是不是某个源特别慢如果错误率集中在某个源先看它的认证或限流策略如果缓存命中率很低可能是查询很多样化缓存价值有限。遇到问题时我建议按这个顺序排查先看现象是超时、报错、无结果还是结果乱序。再看日志来源是否有返回、有没有失败记录、耗时是否异常。再看输入query 是否被正确解析用户权限上下文是否传递完整。再看配置超时时间、并发数、限流参数是否合理。最后看连接器目标系统的接口是否变更返回字段是否仍然兼容。不要一开始就改排序算法。很多时候问题出在更基础的地方比如配置里数据源顺序错了或者连接器一直用的旧凭据。6.4 常见坑和应对顺序多源检索的坑总结起来集中在几个固定位置。第一个坑是连接器超时导致所有结果都失败。应对办法是每个源单独设超时并且失败时只丢弃该源不影响整体返回。第二个坑是结果字段不一致比如有的源返回 content有的源返回 body不统一就会导致摘要为空。应对办法是严格走统一数据模型所有连接器出口都转换完再接主流程。第三个坑是权限过滤放在排序之后导致某些低权限结果被挤到前面之后才被过滤掉浪费了 top-k 名额。更稳妥的做法是权限过滤放在排序之前或者至少放在截断之前。第四个坑是重复结果消耗了大量 token特别是跨源内容高度重复时。应对办法是接新源之前先做一次重复率测试判断该源有没有独立信息价值。第五个坑是数据源返回结果数量差异过大。某个源每次返回 200 条另一个源只返回 5 条如果不做归一化前者会主导整个结果。应对办法是给每个连接器设置返回上限比如统一最多返回 20 条再进入合并层。实际做下来我印象最深的一点是联邦搜索的价值不在架构炫不炫而在接口稳定、结果可解释、权限兜得住。单条查询能跑通只是起点真正要盯的是批量稳定性、失败可恢复和权限不越界。先把这几件事做扎实再把新数据源一个个接进来比一开始就追求“全部接入”要可靠得多。
返回列表