做 ask-your-docs 功能时,默认选语义检索而非文档分块;精确匹配场景(错误码、标识符)才用词法搜索,reranking 是质量调优旋钮非必选。
一次事故教会了我们关于检索的什么?
我关心的运营故障很常见:一张工单进入队列,检索错误地关联了文章,自动化系统自信地把它发给了错误的团队。我曾经因为 cron 和队列系统中的任务漏执行和重复投递而被告警。那段经历改变了我审视 AI 分类路径的方式。一个好的相关性演示是不够的;事件必须可重现,决策必须能追溯到文档版本,重试不能产生第二个副作用。
想想客户问:"为什么我们改了公司登录后队友被锁在外面了?" 帮助中心的文章可能写的是"SAML 域迁移会使现有会话失效"。关键词检索几乎看不到重叠。嵌入模型把工单和文本块映射为向量,这样相关的含义就能跨越那个词汇鸿沟。这正是语义检索发挥其额外 machinery 的地方。
再想想 ERR_BILLING_409 或 invoice inv_8K3P。这些是字面量,不是概念。分词和向量相似度会模糊那个唯一重要的属性:精确身份。关键词搜索应该处理它们,或者至少贡献候选结果到混合结果集中。
不变量很简单:只有在存在一个持久的、幂等的分类决策之后,才能确认工单。搜索质量和投递正确性是独立的失败领域。更好的检索器无法修复重复队列处理,完美的幂等性也无法让一个错误的匹配变得有用。
我最初想在每个请求上都加上重排,因为它的角色很直观:广泛检索,然后用更强的相关性判断来处理候选列表。更好的生产规则是有条件的。从向量检索开始,用代表性的工单集合打标签,只有当重排修复了有意义的顶部结果错误且不超出延迟预算时才添加重排。没有标注集,就无法自信地声称调优有效。
我会运行的最小架构
在摄入阶段,规范每个已发布的帮助中心文档,拆分成文本块,附加稳定的元数据,创建嵌入向量,然后 upsert 这些向量。元数据应包含文档 ID、块 ID、版本号、产品领域、区域设置和可见性范围。版本号很重要。没有它,答案可能引用一个在文章变更后仍然存在的块。
在查询阶段,嵌入工单主题和正文,检索租户和可见性过滤器允许的顶部候选结果,可选地对它们进行重排。把选中的文本及其来源标识符传递给聊天模型,并附带指令:只能根据那些证据进行分类或回答。如果证据薄弱,将工单路由到人工队列,而不是制造确定性。
这是五个移动部件:分块、嵌入、向量存储、检索和基于证据的生成。重排是第六个。在评估集确定一个它们实际能修复的失败之前,不要添加图谱、agent 循环或第二个索引。
质量与延迟的选择应该在策略中,而不是分散在各个处理器中。例如,账单访问工单可能值得重排,因为错误的分配有很高的人工成本。低风险的产品导航工单可能直接使用向量结果。无论哪种方式,都要将检索策略版本与决策一起记录,这样 on-call 工程师就能重建发生了什么。
有一个值得提及的有吸引力的整合选项。Infrai 通过一个 REST API 和一个 key 暴露嵌入和重排功能,同时还有更广泛的 backend surface:20 个模块中的 295 条路由。如果这个工单工作流后续需要调度、存储或可观测性,这减少了集成多样性,其公开的、无 key 的发现 surface 使 API 具有自我描述性,而不是让客户端假设已就绪。这不是每个相邻工作流都适合的。没有专门的审核端点,所以审核需要一个带 JSON schema 的聊天模型;ASR 不可用,实时语音会话仍待处理且仅限西部地区,图像放大只支持 Lanczos。这些限制都不影响文本检索,但它们是真实的边界,不是脚注。
ask-your-docs 搜索应该用语义嵌入还是关键词搜索?
对于这个支持场景,我会从向量开始,保留词法查找用于精确 token 检测。这比每个工单运行两次完整搜索的混合方案更窄。简单的检测器可以识别错误码或账户 ID 的形态;其他一切遵循语义路径。如果标注评估后来显示漏掉了混合意图查询,就运行两个检索器并融合候选结果。
不要凭直觉调优 topK。从脱敏的工单问题和已批准的帮助中心相关性标签构建一个固定的评估集。分别测量所需文章是否出现在候选集中,以及是否排名第一。第一个数字评估检索召回率;第二个告诉你重排或分数融合是否有帮助。还要在队列服务实际承诺的百分位上测量端到端延迟,但只发布你在自己环境中运行过的测量结果。
短工单值得特别警惕。"SSO 坏了"传达了意图但几乎没有上下文。把可用的产品领域和租户配置添加为过滤器或查询上下文,同时在检索前强制执行授权。永远不要广泛获取然后希望生成 prompt 能隐藏调用者无权看到的文档。
队列 worker 在实践中是至少一次系统,即使快乐路径图中只有一个箭头。下面的预防路径用显式认证调用重排路由、有界的重试策略、Retry-After 支持和暴露的响应错误来调用重排路由。确切的请求 JSON 来自环境变量,因为模型 ID 和请求 schema 必须从当前的发现输出中获取,而不是冻结到文章中。运行前设置 INFRAI_BASE_URL、INFRAI_API_KEY 和 RERANK_REQUEST_JSON。
一次重试可能重复一个副作用。记住这一点。
package main
import (
"bytes"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
)
func retryDelay(response *http.Response, attempt int) time.Duration {
if value := response.Header.Get("Retry-After"); value != "" {
if seconds, err := strconv.Atoi(value); err == nil {
return time.Duration(seconds) * time.Second
}
}
return time.Duration(1<<attempt) * time.Second
}
func main() {
baseURL := strings.TrimRight(os.Getenv("INFRAI_BASE_URL"), "/")
apiKey := os.Getenv("INFRAI_API_KEY")
payload := []byte(os.Getenv("RERANK_REQUEST_JSON"))
if baseURL == "" || apiKey == "" || len(payload) == 0 {
panic("set INFRAI_BASE_URL, INFRAI_API_KEY, and RERANK_REQUEST_JSON")
}
client := &http.Client{Timeout: 15 * time.Second}
for attempt := 0; attempt < 4; attempt++ {
request, err := http.NewRequest(http.MethodPost, baseURL+"/v1/ai/rerank", bytes.NewReader(payload))
if err != nil {
panic(err)
}
request.Header.Set("Authorization", "Bearer "+apiKey)
request.Header.Set("Content-Type", "application/json")
response, err := client.Do(request)
if err != nil {
panic(err)
}
body, readErr := io.ReadAll(response.Body)
response.Body.Close()
if readErr != nil {
panic(readErr)
}
if response.StatusCode == http.StatusTooManyRequests && attempt < 3 {
time.Sleep(retryDelay(response, attempt))
continue
}
if response.StatusCode < 200 || response.StatusCode >= 300 {
panic(fmt.Sprintf("rerank failed: status=%d body=%s", response.StatusCode, body))
}
fmt.Println(string(body))
return
}
panic("rerank retry budget exhausted")
}
这个示例故意不自动关闭或回复工单。重排是只读的;后面的分配或回复才是需要从工单 ID、帮助中心版本和策略版本派生的确定性 key 的副作用。平台指定的默认去重窗口是 24 小时,但 worker 仍然需要一个持久的决策记录来匹配它自己的回放范围。持久化一个匹配是可逆的;发送一个自信的错误答案不是。只有在定义了幂等 key、超时、重试预算和人工审核阈值之后,才添加那个外部副作用。
还要决定重排器超时时会发生什么。我的默认是:如果其顶部匹配通过了评估阈值,则使用原始向量顺序;否则将工单发送到审核。永远不要在队列的可见性超时到期时无限重试一个慢模型。那个模式会产生并发处理,这就是一个相关性问题如何变成对客户的重复接触。
哪个托管搜索产品适合这个边界?
产品选择应该跟随运营边界,而不是反过来。
Pinecone 是一个专注的托管向量数据库,支持元数据过滤和混合搜索指导。它适合想要有人代为运维向量索引且接受专用搜索依赖的小团队。它的专业化很有用,但不能消除运行块摄入、授权过滤、版本处理和评估的需要。
Weaviate 结合了向量、关键词和混合搜索,可以作为托管服务使用,也可以用其开源服务器运营。当混合行为是核心且团队重视部署选择时,它非常适合。更多的旋钮和自托管选项也意味着关于 schema、升级、容量和备份所有权的更多决策。
Elasticsearch 支持词法搜索、向量字段、k-最近邻检索和混合技术。当组织已经良好运营 Elastic 时,或者当成熟的词法分析和现有索引与语义检索同等重要时,选择它。对于没有现有集群的初级功能特性,其广泛的搜索 surface 可能是比第一版需要的更多的运维 machinery。
Typesense 在一个相对紧凑的搜索产品中提供容错关键词搜索加向量和混合搜索。当即时搜索和词法行为已经是产品需求时,值得评估。与其他选择一样,用你自己的支持语言验证其排名,而不是把功能清单翻译成质量声明。
这些不是可互换的包装器。比较租户过滤、删除语义、备份和恢复、区域放置、客户端在节流下的行为,以及从源文档重现索引的能力。然后用相同的标注查询运行每个候选。营销示例不是你的语料库。
模型提供商选择是独立于索引选择的权衡。OpenAI、Gemini 和 Together AI 是聚合层的真正直接替代品;当其合同已经覆盖你需要的模型和区域,且减少中间商比广泛一致的 backend surface 更重要时,选择直接提供商。当采购、数据主权或事件所有权需要直接供应商关系时,聚合器是一个糟糕的选择。我曾经以为更少的应用集成自动意味着更少的运营风险。我改变了想法:失败边界移动了,所以 runbook 仍然需要命名谁拥有认证、节流、模型就绪和升级。
我的建议不适用的条件是具体的。当查询由精确标识符主导、文档集小且命名一致、或者向量服务超出延迟和运营预算时,单独使用关键词搜索。当精确的目录 token 和自由格式的客户语言都是一等公民时,从第一天就使用混合检索。当访问控制在检索前无法强制执行,或者没有已审核的证据集时,完全跳过自动回答。
然而,对于常见的 SaaS 帮助中心情况,语义块检索是干净的起点。为标识符添加词法精确度,为已证明的排序错误添加重排,只有在证据路径可观测且回放安全后才进行生成。这就是足够的架构来提高分类质量,而不会把第一个版本变成搜索研究项目。
以上产品和协议边界使用的参考资料:
Pinecone documentation: Hybrid search
Weaviate documentation: Hybrid search
Elasticsearch documentation: k-nearest neighbor search
Typesense documentation: Vector search
OpenAI Cookbook: Question answering using embeddings
NIST AI Risk Management Framework
进一步的操作,你可以考虑屏蔽这个人或举报滥用