AI 应用后端工程化:从原型到可交付系统 · 第 05 篇 · 第二章 · 知识与工具
串起装载、清洗、切片、嵌入、索引、检索与生成,解释每一环如何影响最终答案。
把一份 PDF 存进向量数据库,再让模型根据检索结果回答问题,看起来就是 RAG。这个演示能说明方向,却很容易掩盖真正决定质量的环节:文档是否解析完整、切片是否破坏语义、索引是否保留来源、查询是否召回正确证据、模型是否明确区分证据与猜测。
RAG 的全称强调“检索增强生成”,但工程上更适合把它看成一条数据供应链。模型只是链条末端的消费者。上游任何一次污染,都会在答案里以更自然、更难察觉的方式被放大。
从原料到证据
flowchart LR
File[原始文档] --> Parse[解析]
Parse --> Clean[清洗]
Clean --> Chunk[切片]
Chunk --> Embed[嵌入]
Embed --> Index[(索引)]
Question[用户问题] --> Query[查询改写]
Query --> Retrieve[召回]
Index --> Retrieve
Retrieve --> Filter[过滤/重排]
Filter --> Context[带来源证据]
Context --> Generate[生成答案]
这条链可以分为离线与在线两部分。离线阶段处理文档、生成向量并写入索引,追求可重复、可恢复;在线阶段接收问题、召回证据并生成答案,追求低延迟和相关性。把两者混在一个 HTTP 请求里,上传大文件时就会遇到超时,也很难解释处理到了哪里。
文档加载不等于读到了正文
PDF 可能是文本、扫描图片或混合排版;网页可能把导航、版权声明和正文一起抓下来;表格的行列关系在纯文本中容易丢失。加载器应该输出统一文档对象,同时保留来源、页码、标题层级、更新时间和权限等元数据。
元数据不是装饰。回答“这个规则来自哪里”需要页码和链接;过滤过期制度需要版本日期;多租户隔离需要所有者字段。只存一段文本和向量,之后很难补回这些信息。
from dataclasses import dataclass
@dataclass(frozen=True)
class Document:
text: str
source: str
page: int | None
version: str
owner_id: str
统一对象也让加载器可以替换。PDF、Markdown 和网页的解析方式不同,后续切片与索引却可以只依赖 Document 契约。
切片是一种检索假设
切片太大,召回结果含有大量无关内容,占用上下文;切片太小,定义与限定条件被拆开,模型只看到半句话。固定字符长度简单稳定,按标题和段落切分更接近语义,代码与表格则需要专门规则。
重叠窗口能缓解边界断裂,但会制造重复证据并增加索引成本。合理参数需要从真实问题反推:用户通常问一个术语、一个流程,还是跨章节比较?没有统一的“最佳 500 字”。
每个片段还应保存父文档 ID 和在原文中的位置。检索命中后,可以返回相邻片段或重新展开完整小节,而不是把所有信息永久切碎。
向量相似不是答案正确
嵌入把文本映射到高维空间,相近向量往往表达相似语义,但“相似”不等于“能回答”。问题“如何取消订单”可能召回“如何创建订单”,因为两者共享大量词义。精确编号、错误码和人名则可能更适合关键词检索。
因此成熟系统常组合向量召回与全文检索,再用元数据过滤和重排模型缩小结果。还要设置最低质量门槛:没有足够证据时允许回答“不确定”,而不是把最接近的片段强行当答案。
{
"query": "合同可以提前终止吗",
"evidence": [
{"source": "agreement-v3.pdf", "page": 12, "score": 0.82},
{"source": "termination-faq.md", "page": null, "score": 0.77}
],
"answer_policy": "仅依据 evidence 作答,并标注来源"
}
把来源连同片段交给模型只是第一步。输出协议还应要求引用,应用最好验证引用 ID 确实来自本轮候选,防止模型虚构一个看似可信的文档名。
失败往往发生在更新时
第一次导入容易成功,第二版文档上线才暴露生命周期问题。旧片段是否删除?正在查询时索引能否同时更新?同一文件重复上传会不会产生两份结果?
可以为每次处理生成版本,先写入新版本索引,全部成功后再切换“当前版本”指针,最后异步清理旧数据。处理任务使用文件哈希保证幂等,同一内容重复提交直接复用结果。删除父文档时,片段、向量和缓存都应能沿关联关系清理。
另一个危险是权限变化。文档从公开改为部门可见,索引若没有及时同步,向量检索仍可能把旧片段返回。权限过滤必须发生在召回阶段,而不是等模型生成答案后再删文字。
如何知道 RAG 真的变好了
准备一组真实问题,标注每个问题应该命中的文档和可接受答案。分别测量召回率、前几名排序、答案引用正确率和无答案时的拒答率。只看最终回答“像不像人话”,无法定位问题来自检索还是生成。
也可以做一次反事实测试:从上下文中拿掉正确证据,模型是否承认无法回答?如果它仍然自信给出同一答案,说明生成约束不足,测试集也可能没有覆盖幻觉风险。
RAG 方法还能迁移到商品搜索、客服知识、代码检索和合规问答。变化的是文档类型与评价指标,不变的是数据生命周期、证据来源和离线评估。
练习:选三份带版本号的短文档,设计五个问题,其中一个明确无答案。记录切片方式、召回前五项、引用和拒答结果,再修改切片参数做对比。最终产物应是一张可复查的评估表,而不是一次聊天截图。
四次提交对应的链路扩展
a6c2fec:增加向量存储服务与配置,建立索引落点。28b0bc5:加入文档加载和切分,补齐索引前处理。ae1302d:加入检索与查询路径,数据开始服务在线问题。30d7352:把检索结果连接到问答模型,形成端到端输出。
顺序很有启发:先有存储,再补数据处理,然后构建检索,最后连接生成。反过来只盯着最终回答,很容易把上游缺陷误判成模型能力问题。