RAG从玩具到能用,中间差了3个我踩过的坑

0 阅读1分钟

作者:Java宋转AI
标签:RAG / AI Agent / 生产级架构
阅读时间:约8分钟

背景

RAG问答demo谁都能跑通,5分钟教程满天飞。向量数据库一搭,Embedding模型一调,Prompt一塞,出来个能跑的对话界面,成就感拉满。

但我真正上线后发现,demo和生产的差距不是调参数能补的。

是什么差距?是3个架构层面的决策

每个决策都是踩了坑才想明白的。今天不聊怎么配置,聊"为什么这么选"。

决策1:为什么不能只靠语义检索

我一开始觉得向量检索是银弹。语义理解嘛,把Query往里一丢,语义相近的文档就召回来了,比关键词匹配聪明多了。

直到用户问了一句:"SKU 123456还有货吗"

向量检索召回的第一条是退货政策文档。

你可能会问:退货政策和库存查询,语义上完全不搭边啊,怎么会召回来?

我debug了半天发现,embedding模型在训练时见过太多"SKU"和"退货"、"退款"、"售后"这些词共现,它们在向量空间里的距离确实很近。语义检索的盲区就在这——它学的是语义的统计相关性,不是业务语义的精确匹配。

更坑的是,库存是实时状态。你向量库里存的是昨天的库存快照,今天早卖光了,RAG告诉用户"有货",这笔订单就砸了。

我的第一反应为什么不对

调相似度阈值。从0.7调到0.8,调到0.9。结果退货政策文档确实下去了,但正经的库存查询文档也召不回来了。

阈值调参是一个局部最优解,根本问题没解决。

真正的解法

给RAG和工具调用划清楚边界。

java

// 库存查询 -> 工具调用
@Tool("getInventory")
public InventoryResult getInventory(String skuId) {
    // 实时查询库存系统
}

// 退货政策 -> RAG
public String getReturnPolicy(String productId) {
    // 查向量库
}

库存查询、价格查询、订单状态——这些是结构化查询,是写操作,是实时数据。它们不该走RAG。

那非实时的事实性知识怎么办?我的做法是语义+词汇双路检索,0.7/0.3权重合并

java

public class HybridRetriever {
    private static final double SEMANTIC_WEIGHT = 0.7;
    private static final double LEXICAL_WEIGHT = 0.3;
    
    public List<Chunk> retrieve(String query) {
        double semanticScore = semanticSearch(query);
        double lexicalScore = lexicalSearch(query);
        double combinedScore = semanticScore * SEMANTIC_WEIGHT 
                             + lexicalScore * LEXICAL_WEIGHT;
        // ...
    }
}

词汇检索专门处理SKU、订单号、型号这些业务专有名词。语义检索处理业务语义理解。两者互补,盲区就少多了。

深层的思考

RAG不是万能的。实时数据、结构化查询、写操作,这些不该进RAG。

给RAG划清楚边界,比优化检索算法更重要。

决策2:为什么改了文档,但RAG还在召回旧内容

这个坑我踩得比较隐晦。

一开始我建知识库,就是把文档chunk之后扔进向量库,简单粗暴。后来业务方说文档更新了,我去向量库里查,发现召回来的还是旧内容。

为什么?因为向量库里的内容不会自动跟着源文档更新

你改了源文档,向量库里的向量还是旧的。两条数据完全不在一个版本上。用户问的是新版文档的内容,但RAG召回的是旧版文档的向量,这答案能对吗?

更坑的情况

我们有好几个业务线,共用一个向量库。A业务线的运营更新了他们的知识库,但B业务线的用户在查询时,偶尔会召回A业务线的文档。

原因很简单——向量检索没有加业务隔离的filter,所有文档都在同一个向量空间里,距离近就召回来了。

真正的解法

给每条知识打元数据标签,检索时加filter。

sql

CREATE INDEX idx_rag_chunks_metadata 
ON rag_chunks USING GIN (metadata);

检索的时候带上业务隔离:

sql

SELECT * FROM rag_chunks 
WHERE embedding <=> %s < 0.6
  AND metadata->>'bizLine' = 'product-a'
  AND metadata->>'tenantId' = 'tenant-123'
  AND metadata->>'knowledgePackId' = 'kp-20240501'
  AND metadata->>'promptRevision' = 'v2.3';

上线前对三个ID对账:knowledgePackId、promptRevision、bizLine+tenantId。三个对不上就不允许上线。

实际的向量化pipeline

java

@Data
public class ChunkMetadata {
    private String bizLine;        // 业务线
    private String tenantId;       // 租户ID
    private String knowledgePackId; // 知识包版本
    private String promptRevision;  // Prompt版本
    private Long createdAt;
    private String sourceUri;
}

知识包带版本号,离线任务推版本到pgvector,检索时filter和索引保持一致。

深层的思考

知识库不是"丢进去就行"的,它需要和代码一样的版本管理思维

代码

知识库

git

knowledgePackId

CI

对账流程

环境隔离

tenant隔离

决策3:为什么召回率很高,但答案还是不准

这个坑我一开始以为是Prompt的问题。

我调Prompt调了两个星期,换了各种写法,加了各种约束条件,答案还是时准时不准。后来我发现,问题根本不在Prompt,在召回的内容

我统计了一下,top-k=5召回了5条文档,但真正和用户Query相关的只有1条。其他4条是什么?是噪声。LLM被这4条噪声带偏了,给出了一个看起来像那么回事但实际不对的答案。

更坑的情况

有一次运营把整页商详HTML丢进了RAG,一条chunk就是200KB。top-5召回来,Prompt直接超过context limit,LLM报错。

那天半夜2点我爬起来修bug,看到那个200KB的chunk,整个人都麻了。

真正的解法

RAG不该传全文,只传摘要+链接。

向量化pipeline只提取三段内容:

java

@Data
public class Chunk {
    private String chunkId;
    private String summary;        // 摘要:概括性描述
    private String coreParams;    // 核心参数:芯片、屏幕、电池...
    private String afterSales;     // 售后条款:退货政策、保修...
    private String fullContentUri; // 完整内容存对象存储,按需拉取
}

LLM拿到的是精炼的上下文,不是200KB的HTML噪声。

自适应阈值检索

top-k是固定的,但Query的难度是变化的。

java

public class AdaptiveThresholdRetriever {
    private static final double HIGH_THRESHOLD = 0.8;
    private static final double LOW_THRESHOLD = 0.6;
    
    public List<Chunk> retrieve(String query, double semanticScore) {
        if (semanticScore >= HIGH_THRESHOLD) {
            return directPush(query); // 高置信度直接推
        } else if (semanticScore <= LOW_THRESHOLD) {
            return lexicalFallback(query); // 低置信度回退词汇检索
        } else {
            return hybridRerank(query); // 中间段走混合重排
        }
    }
}

简单Query一条就够了,复杂Query需要多条。阈值自适应,不用手调。

深层的思考

RAG的本质是"给LLM提供恰到好处的上下文"。

少了,LLM产生幻觉,开始胡编乱造。

多了,噪声淹没信号,LLM被带偏。

上下文的质量和数量同样重要,甚至更重要。

技术栈参考

本文涉及的技术实现:

  • Spring AI Alibaba + pgvector

  • DashScope text-embedding-v3(1024维)

  • ContentDraftRagRetriever:自适应阈值 + 混合重排 + 缓存

  • 工具调用 vs RAG 的边界划分

总结

RAG不是"丢进去就行"的,它需要和代码一样的工程思维。

  • 版本管理要像git一样严谨

  • 边界划分要像接口设计一样清晰

  • 上下文控制要像性能优化一样斤斤计较

demo和生产的差距,不在调参,在这些架构决策。

关注我,少走3个月弯路