全库 RAG 问答接口怎么设计:为什么我没有把 postId 做成可选参数
RAG实践

全库 RAG 问答接口怎么设计:为什么我没有把 postId 做成可选参数

在做知识库问答时,我遇到一个看似很小、但很影响后续维护的问题:

单篇文章问答和全库问答,要不要共用一个接口?

最简单的写法是把 postId 做成可选参数。

有 postId:查单篇文章
没有 postId:查全库

但我最后没有这么设计,而是倾向于把两个场景拆成两个明确入口。

1. 两个场景的语义不一样

单篇文章问答的含义是:

只围绕当前这篇文章回答

全库问答的含义是:

从整个知识库里找相关资料回答

它们看起来只是过滤条件不同,但产品语义其实不一样。

如果把 postId 设计成可选参数,前端或调用方很容易误用:

  • 明明想问单篇,却漏传了 postId,结果查了全库。
  • 明明想问全库,却传了错误的 postId,结果检索范围被限制。
  • 后端逻辑里到处判断 postId == null,接口含义变模糊。

所以我更喜欢让路径表达语义。

2. 更清晰的接口拆分

可以设计成这样:

场景 接口 含义
单篇问答 GET /api/v1/knowposts/{id}/qa/stream 只检索指定文章
全库问答 GET /api/v1/knowposts/qa/stream 检索整个知识库

这样前端看到接口就知道自己在调用什么模式。

后端 Controller 也不需要根据可选参数猜意图。

3. 服务层也应该表达意图

接口拆开之后,服务层也可以拆成两个入口:

streamPostAnswer(postId, question, topK)
streamGlobalAnswer(question, topK)

检索层也是一样:

retrieveForPost(postId, question, topK)
retrieveGlobal(question, topK)

这不是为了多写几个方法,而是为了让调用链更可读。

单篇链路:

Controller 单篇入口
-> QueryService 单篇回答
-> RetrievalService 单篇检索
-> 添加 postId 过滤器
-> RRF 融合
-> 生成回答

全库链路:

Controller 全库入口
-> QueryService 全库回答
-> RetrievalService 全库检索
-> 不添加 postId 过滤器
-> RRF 融合
-> 生成回答

差异集中在检索范围,其他逻辑继续复用。

4. 为什么不用“一个方法全包”

初学时我会觉得:能复用就尽量复用,一个方法里判断参数不是更简单吗?

后来发现,复用不等于把所有逻辑塞进一个方法。

如果一个方法同时代表两种业务语义,它的名字就会越来越抽象,后续改动也越来越危险。

比如:

search(question, postId, topK)

这个方法看不出来:

  • 是否会触发单篇索引检查。
  • 是否允许全库检索。
  • postId 为空是不是合法。
  • 未来加入权限时该怎么判断。

相比之下,两个命名明确的方法更长,但更不容易误用。

5. topK 保留给前端,但要限制范围

topK 是一个值得保留的参数。

因为它直接影响检索观察:

  • 太小:可能漏掉正确 chunk。
  • 太大:上下文噪声变多,模型更容易跑偏。

所以可以让前端传 topK,但后端要限制范围,例如 1-20。

这样既能调试,也不会把检索规模放飞。

6. 这次设计给我的经验

接口设计不是只看“能不能少写代码”。

更重要的是:

路径能不能表达业务语义?
调用方会不会误用?
服务层方法名能不能说明意图?
后续加权限、记忆、debug 时会不会越来越混乱?

对 RAG 来说,单篇问答和全库问答是两个不同产品能力。

所以我更愿意让它们在接口上分开,在内部复用共同的检索和回答流程。

这是一种更稳的工程选择。

你可能也会喜欢...

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注