框架与协议迁移
18

LangChain4j RAG 适配实验:复用现有业务契约

LangChain4j 自带 ContentRetriever、RetrievalAugmentor 和 Embedding Store(向量存储抽象),重新搭一套独立 RAG 很快。RAG 是检索增强生成:先从知识库找出相关片段,再把片段交给模型回答。

现有知识服务已经管理了文档版本、ACL(访问控制列表)、有效期、索引任务和引用规则。若新框架再建一套向量库,同一份知识就会有两个事实来源。

框架迁移不应该改写知识数据所有权。否则 tenant、department、发布状态和 TopK 前过滤可能在新链路中被绕过,短期代码独立,长期却要维护两套相互冲突的知识库。

隔离 lab 中定义了一个最小 KnowledgeSearchPort,它保留现有检索链路的身份范围、查询和 TopK 语义,但不直接依赖 Knowledge Service 内部类。LangChain4j 适配器先调用该端口,再把返回的片段转成框架内容。直接运行入口用受控内存语料固定检索事实,再调用真实模型生成带来源回答;它用于看清适配顺序,不替代阶段 03 已经跑通的 pgvector 与 ACL 链路。

先固定企业 RAG 的业务契约

一个可替换的检索端口至少需要:

public interface KnowledgeSearchPort {
    List<KnowledgeSnippet> search(
            KnowledgeAccessScope scope,
            String query,
            int topK);
}

KnowledgeAccessScope 是一次请求的不可变访问范围,包含 tenant、subject 和部门集合。正式环境由公司鉴权适配器建立,本地与 lab 使用固定测试身份。TopK 表示取排名最靠前的 K 个候选片段。KnowledgeSnippet 返回 sourceId 与低敏正文,正式接入时还应带文档版本、标题、有效期和相关性分数。

正式适配器要调用 Knowledge Service 的应用端口或版本化 HTTP API,不在 lab 里复制检索实现。RRF 是倒数排名融合,用来合并多路检索排序;Rerank 则会对候选结果再排一次序。Knowledge Service 继续负责:

  • tenant、department、状态和有效期在 TopK 前过滤;
  • 向量、关键词、RRF 和 Rerank;
  • 索引版本与 Embedding 模型隔离;
  • 文档发布状态和冲突处理;
  • 检索指标与坏案例。

框架适配器只消费结果。

LangChain4j 通过业务检索端口取证据

配套代码为每次调用创建一个绑定 AccessScope 的检索器:

List<KnowledgeSnippet> snippets = searchPort.search(scope, question, topK);

ContentRetriever retriever = query -> snippets.stream()
        .map(snippet -> Content.from(
                "来源 " + snippet.sourceId() + "\n" + snippet.text()))
        .toList();

KnowledgeAssistant assistant = AiServices.builder(KnowledgeAssistant.class)
        .chatModel(chatModel)
        .contentRetriever(retriever)
        .build();

这段实验代码强调调用顺序:先调用 KnowledgeSearchPort,再构造 ContentRetriever。ACL 是否真的在 TopK 前执行,取决于正式检索实现;这里的 SearchPort 只记录收到的 scope,不能证明数据库权限过滤。正式接入时可以让 Retriever 自己调用端口,但它必须携带不可变的请求级 scope,不能从模型参数或用户 JSON 中读取 tenant。

每次调用使用不可变 AccessScope

最容易出现的错误是把当前 tenant 放进 ThreadLocal 或单例字段,再让共享 Retriever 读取。同步请求可能暂时可用,进入 Reactor、异步 Tool、多线程或并行评测后就可能串租户。

更稳妥的方式包括:

  • 每次调用创建不可变 Retriever;
  • 使用框架支持的 invocation metadata,并在边界处显式写入;
  • 通过请求作用域组件传递;
  • 在业务端口调用前完成检索,再把结果交给 RAG 组件。

无论采用哪种方式,scope 都必须来自服务端受信身份,不能从问题或模型输出生成。模型只能看到必要的租户上下文,不能决定访问范围。

框架替换不改变 ACL 顺序

框架替换不能改变 RAG 权限顺序

正确顺序是:

Trusted Identity
  -> ACL Candidate Query
  -> TopK / Hybrid / Rerank
  -> Authorized Snippets
  -> Prompt Augmentation
  -> Model Answer
  -> Citation Validation

错误顺序是先全库向量 TopK,再在 Java 中过滤无权文档。即使最终没有把正文返回用户,候选、日志、Rerank 请求或模型上下文都可能接触越权内容;过滤后候选不足还会伤害召回。

换框架时应运行专门的 ACL 数据集:同一个问题在不同 tenant、部门和角色下预期 sourceId 不同。任何未授权文档进入候选都应计为安全失败,而不是普通质量扣分。这类数据集不在当前 lab 测试范围内。

ContentRetriever 只做结果转换

框架组件擅长组合,但不应拥有:

  • 文档生命周期和发布审批;
  • 内容摘要、去重和冲突策略;
  • 对象存储与解析任务;
  • tenant 和部门授权模型;
  • 索引回填与版本切换;
  • 引用是否有效的业务规则。

这些能力要么属于 Knowledge Service,要么属于明确的基础设施适配器。把它们全部藏在 EmbeddingStoreContentRetriever Builder 中,代码看起来简短,却失去了可治理入口。

从简单检索开始选择 RAG 组合

contentRetriever 会创建默认 RetrievalAugmentor,适合一条简单检索链。需要 Query Rewrite、多路检索、聚合、动态路由或自定义注入时,可以配置 RetrievalAugmentor。

升级条件仍由坏案例驱动:

  • 用户问题省略关键实体,考虑 Query Rewrite;
  • 关键词和语义各自漏召回,考虑混合检索;
  • 正确文档在候选内但排序靠后,考虑 Rerank;
  • 问题属于不同知识域,考虑路由;
  • 多个片段冲突,先做业务冲突处理,不让模型自行选一个。

不要因为框架提供高级 RAG 就一次性启用。每增加一个模型步骤,都会增加延迟、成本、失败点和评测维度。

受控检索内容进入 Prompt

默认内容注入器会把片段加入用户消息。企业项目应控制格式:

[sourceId=FIN-2026-01]
title=差旅报销制度
effectiveAt=2026-01-01
content=审核通过后两个工作日内付款。

来源编号是后续引用校验的锚点。不要只注入正文,否则模型返回的“根据制度”无法追到具体文档。

片段内容属于不可信数据。文档中出现“忽略系统规则”“调用退款工具”等文字时,只能作为被引用内容,不能提升为系统指令。这类通过数据影响模型指令的攻击叫 Prompt Injection(提示注入)。Prompt 要明确知识区边界,并用提示注入样例做回归。

引用仍由应用层校验

模型回答包含 FIN-2026-01,服务端要检查它是否属于本次检索候选。若模型引用未知 sourceId、失效版本或其他 tenant 文档,应拒绝或重新生成。

配套 PolicyAnswer 同时返回答案与 sourceIds:

return new PolicyAnswer(
        answer,
        snippets.stream().map(KnowledgeSnippet::sourceId).toList());

当前 PolicyAnswer.sourceIds 是这次检索的候选来源列表,不是从模型回答中解析出来的引用。现有知识服务已经实现引用成员校验、时效和冲突规则,迁移时必须继续使用,不能因为 AI Service 返回 String 就省略。

RAG 缓存键包含权限与版本

可缓存查询向量、低敏检索结果或最终答案,但缓存键至少考虑:

tenantId
departmentScopeHash
subjectPolicyVersion
normalizedQuery
indexVersion
embeddingModelId
retrievalPolicyVersion
promptVersion
model

省略权限字段会串数据,省略索引或 Prompt 版本会在发布后继续返回旧答案。最终答案缓存还要考虑文档撤回和用户反馈。

用一次调用看清当前适配边界

CapturingKnowledgeSearchPort 记录收到的 scope 和 TopK,捕获 ChatRequest 的模型则显示来源怎样进入 Prompt:

tenant-a
departments=[finance]
topK=4
sourceId=FIN-2026-01

当前实验没有构造第二个 tenant,也没有让模型返回未知 sourceId。跨租户隔离和无依据答案拒绝仍要在完整链路中覆盖:

  • tenant 与部门交叉;
  • 文档失效和新版本发布;
  • 空召回与拒答;
  • 多证据与冲突证据;
  • Injection 文档;
  • 候选过长与 Token 预算;
  • Retriever 超时、Rerank 超时和降级;
  • Recall@K、MRR、重复率和 p95。

这里的 Recall@K 衡量前 K 个候选是否找回应有证据,MRR 关注第一个正确结果出现的位置,p95 表示 95% 请求能够完成的延迟上界。

只断言最终答案相似,无法发现 ACL 或 sourceId 已经变化。

跨服务继续使用版本化 HTTP API

如果 LangChain4j 适配器部署在另一个服务,它应通过 Knowledge Service 的认证 HTTP/OpenAPI 接口查询。不要引入 Knowledge Service 内部 Repository 或实体 JAR。

HTTP 调用需要 delegated token、audience、scope、超时、连接池和稳定错误码。评测端点使用独立权限,不能让普通回答 Token 访问全量检索调试信息。

框架共存期间,现有实现和实验实现都调用同一公开 API,才能比较结果;一条走生产 SQL,一条读本地文件,比较没有意义。

这次适配只复用检索端口

LangChain4j lab 在 TenantScopedRagAdapter 中创建带 ContentRetriever 的 AI Service。运行 LangChain4jRagLabApplication,可以看到真实模型根据受控片段回答,并在 PolicyAnswer 中返回 sourceId。

这里使用可控 SearchPort 与真实 ChatModel:前者让 scope、TopK 和 sourceId 可以直接核对,后者让读者看到框架确实完成了 Prompt 增强与生成。认证、ACL SQL、引用成员校验和真实检索继续由 Knowledge Service 拥有。迁移时把 SearchPort 接到它的版本化 API,再使用相同身份范围、索引和 Golden Set 比较结果。

RAG 框架可以替换,知识服务的数据所有权不能替换。文档生命周期、ACL、索引和引用校验保持不变后,才有条件比较不同框架的开发效率和质量。Tool Calling 与结构化输出一旦进入自动执行链路,仍要接受同样的身份、参数和业务规则校验。

参考资料:

搜索全部 52 篇文章。

查看配图