记忆与 RAG 实战:什么时候该用向量库,什么时候不该

作者:

本文是《从零到上线:Agent 应用开发完整路线图》系列第三篇。前两篇讲了整体路线和工具调用设计,这一篇聚焦一个被过度使用的技术——RAG(检索增强生成),搞清楚它到底适合解决什么问题,以及更多时候,一个被忽视的更简单方案就够了。

很多团队的第一反应是:Agent 需要”记忆”或者”知识库” → 上向量数据库 → 做 RAG。这个路径有时候是对的,但相当多情况下,这是在用一个更复杂、更难调试的方案,解决一个本可以用”把数据当工具查询”就能搞定的问题。

这篇文章把两种方案的适用边界讲清楚,并给出两种都能跑的代码示例。

一、先分清两种完全不同的”记忆”

讨论 RAG 之前,要先把”记忆”拆成两个不同的问题,它们的解法完全不同:

短期记忆:当前这一次对话的上下文。模型在多轮对话里记得你前面说过什么,靠的是把历史消息原样塞进 messages 数组——这不需要任何额外组件,上下文窗口本身就是短期记忆。

长期记忆/知识检索:跨会话保留的信息,或者一个庞大到塞不进上下文窗口的知识库(产品文档、历史工单、过往对话记录)。这才是 RAG 真正要解决的问题。

很多人把这两者混为一谈,结果在只需要管理好上下文窗口的场景里,过早引入了向量库。

二、RAG 到底是什么

RAG 的核心循环很简单:

1. 把知识库文档切成小块(chunk),提前转成向量(embedding),存进向量库
2. 用户提问时,把问题也转成向量
3. 在向量库里找出语义最相似的几个文档块
4. 把这几个文档块作为上下文,塞给 LLM 一起生成回答

它解决的问题是:知识库太大,没法每次都整个塞进 prompt,需要先”检索”出最相关的一小部分

下面是一个可以直接跑的最小 RAG 示例,用 chromadb 做向量库(轻量、本地跑不需要额外服务):

import chromadb
import anthropic

client = anthropic.Anthropic()
chroma_client = chromadb.Client()  # 内存模式,实际项目用 PersistentClient 落盘

# 1. 建立知识库集合
collection = chroma_client.create_collection(name="product_docs")

# 2. 灌入文档(实际项目中这一步是离线批处理,文档要先切块)
docs = [
    "退货政策:商品签收后7天内,保持完好可申请无理由退货,运费由买家承担。",
    "会员等级:消费满1000元升级银卡,满5000元升级金卡,金卡享受免运费特权。",
    "发货时效:工作日下单当天发货,周末及节假日顺延至下一工作日处理。",
]
collection.add(
    documents=docs,
    ids=[f"doc_{i}" for i in range(len(docs))]
)

def search_knowledge_base(query: str, n_results: int = 2) -> str:
    """检索最相关的文档块"""
    results = collection.query(query_texts=[query], n_results=n_results)
    retrieved = results["documents"][0]
    return "\n".join(retrieved)

def answer_with_rag(user_question: str) -> str:
    context = search_knowledge_base(user_question)

    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=512,
        messages=[{
            "role": "user",
            "content": (
                f"根据以下知识库内容回答用户问题,如果知识库没有相关信息,"
                f"如实告知不知道,不要编造。\n\n"
                f"知识库内容:\n{context}\n\n"
                f"用户问题:{user_question}"
            )
        }]
    )
    return response.content[0].text

if __name__ == "__main__":
    print(answer_with_rag("金卡会员有什么权益"))

这套流程能跑通,但要在生产环境用好,有几个容易被忽视的细节:怎么切块(chunk size)、检索召回不准怎么办、文档更新了向量库要不要重建——这些是 RAG 真正的难点,后面专门讲。

三、什么时候不该用 RAG:直接把数据当工具查询

回到第一篇文章手写的最小 Agent 循环。如果你的”知识库”其实是结构化数据——订单记录、用户信息、商品库存——直接写一个查询工具,通常比做 RAG 更简单、更准确。

判断标准很直接:这份数据有没有明确的查询键(key)?

  • 有(订单号、用户ID、商品SKU)→ 直接查询,不需要 RAG
  • 没有,是大段非结构化文本,需要”语义相关”才能找到 → 才考虑 RAG
# 不需要 RAG 的场景:有明确查询键
tools = [{
    "name": "get_order_detail",
    "description": "根据订单号查询订单详情(商品、金额、状态、收货地址)",
    "input_schema": {
        "type": "object",
        "properties": {"order_id": {"type": "string"}},
        "required": ["order_id"]
    }
}]

def get_order_detail(order_id: str) -> str:
    order = db.query(order_id)  # 直接精确查询,100% 准确
    if not order:
        return f"未找到订单 {order_id}"
    return f"订单{order_id}:{order.items},金额{order.amount},状态{order.status}"

这种场景下用 RAG 反而是退步:向量检索是”语义相似度”匹配,本质上是模糊检索,而订单号这种结构化查询需要的是精确匹配。把订单数据塞进向量库做语义检索,召回率不仅不会比 WHERE order_id = ? 更好,还会引入”检索到相似但不对的订单”这种本不该存在的错误模式。

一条经验法则:

数据特征推荐方案
有明确查询键(ID、订单号、日期范围)直接查询工具(SQL/API)
数据量小,能完整塞进 system prompt直接放 prompt,不需要检索
大段非结构化文本,需要”语义相关”匹配RAG
知识库会频繁更新优先考虑直接查询(若结构化)或确保有重建索引的流程
用户问题措辞多样、查询意图模糊RAG 配合较宽松的召回数量

实践中很多”看起来需要 RAG”的场景,拆开看其实是结构化数据,换成查询工具能省掉整套向量库的运维成本和召回不准的调试成本。

四、RAG 真正的难点:不是搭起来,是调准

如果你的场景确实是非结构化文本(产品手册、政策文档、历史聊天记录),RAG 是对的方向,但”搭起来能跑”和”检索准确”是两回事。常见的几个坑:

1. Chunk 切分粒度

切得太大,一个 chunk 里混了多个话题,检索命中了但里面 80% 是无关内容,白白占用上下文;切得太小,一句话被拦腰截断,丢失语境。

# 简单但有效的切分策略:按语义边界(段落)切,而不是固定字符数
def chunk_text(text: str, max_chunk_size: int = 500) -> list[str]:
    paragraphs = text.split("\n\n")
    chunks = []
    current = ""
    for para in paragraphs:
        if len(current) + len(para) > max_chunk_size and current:
            chunks.append(current.strip())
            current = para
        else:
            current += "\n\n" + para
    if current:
        chunks.append(current.strip())
    return chunks

按固定字符数硬切是最容易踩的坑,优先按段落、标题这类语义边界切。

2. 检索召回不准怎么办

如果发现模型经常说”知识库里没有这个信息”但实际上文档里明明有,大概率是检索阶段就没召回正确的文档块,而不是模型生成的问题。排查顺序:

  1. 单独测试检索结果,不要直接看最终回答——把 search_knowledge_base 单独跑一遍,人工检查召回的内容是不是真的相关
  2. 检查 chunk 是否切得太碎,导致关键信息被拆散到了不同 chunk 里
  3. 增加召回数量(n_results 调大),让更多候选文档进入上下文,代价是 token 成本上升
  4. 必要时混合关键词检索(BM25)和向量检索,纯语义检索对专有名词、产品型号这类词经常不够准

3. 文档更新了,索引要同步

向量库不会自动感知源文档变了。生产环境必须有一套流程:文档更新 → 重新生成对应 chunk 的向量 → 替换旧向量。这件事容易被忽略,导致 Agent 长期基于过时信息回答。

五、一个完整的判断流程

把上面讲的整理成一张决策图,遇到”要不要上 RAG”的场景直接对照:

这份信息的体量能塞进一次 system prompt 吗?
  └─ 能 → 直接放 prompt,不需要检索
  └─ 不能 ↓

这份数据有明确的查询键(ID/订单号/日期)吗?
  └─ 有 → 写一个直接查询工具(数据库/API),不需要 RAG
  └─ 没有,需要"语义相关"才能定位 ↓

确实是大段非结构化文本,且会被频繁问到 → 用 RAG
  注意:chunk 切分按语义边界、单独测试召回质量、建立索引更新流程

大部分”我们需要做 RAG”的需求,在这张图的前两步就已经有更简单的答案了。RAG 该用的时候确实好用,但它不是”知识库类需求”的默认选项,而是排除了更简单方案之后的选择。


下一篇进入多 Agent 编排实战,讲 Supervisor 模式什么时候真正需要、并行子任务怎么设计,以及一个常见误区:多 Agent 不是”更强”,而是用额外的复杂度换取任务拆解的清晰度,用错场景反而会更难调试。

评论

发表回复

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

湘ICP备2026010540号