02 RAG 引擎核心流程
结论先说:RAG 在这里不是“长期记忆”,而是“证据注入系统” #
很多文章把 RAG 写成一句空话:给大模型外挂知识库。这个说法太粗了。
在 SecurityClaw 这类 Agent 系统里,RAG 真正承担的是三件事:
- 把历史证据和外部知识引入当前决策
- 在模型不知道事实时提供检索支撑
- 在向量检索失效时,保证系统还能降级工作
所以这篇不只是讲 store / retrieve / build_context 三个函数,而是要讲清楚:RAG 为什么是知识层,而不是状态层。
一、先看核心接口:RAG 引擎只有三件事 #
class RAGEngine:
def store(self, text: str, category: str, source: str) -> str:
...
def retrieve(self, query: str, k=5, category=None) -> list[dict]:
...
def build_context(self, query: str, k=5) -> str:
...
这三个函数分别对应三个问题:
- store:什么知识值得留下
- retrieve:当前问题该找哪些证据
- build_context:找到的证据如何变成模型能消费的输入
二、store 解决的不是“保存文本”,而是“保存可检索证据” #
{
"_id": sha256(text)[:32],
"text": "...",
"embedding": [0.1, 0.2, ...],
"category": "anomaly",
"source": "threat_analysis_skill",
"timestamp": "ISO8601"
}
2.1 为什么 _id 用内容哈希 #
因为这一步真正想做的是去重后的证据库,不是原始日志仓库。
好处 #
- 相同内容不会重复写入
- 检索空间更干净
- 降低向量存储冗余
代价 #
- 同一文本在不同语境下可能被压成同一条
- 去重粒度依赖文本规范化方式
这里的 tradeoff 很清楚:SecurityClaw 更在意“不要把重复事实无限灌进知识层”,而不是“保留每一次重复记录”。
三、retrieve 的重点不是向量检索,而是失败后的行为 #
flowchart LR
Q[Query] --> E[Embed query]
E --> K{kNN search}
K -->|成功| R1[top-k semantic hits]
K -->|失败| F[keyword fallback]
F --> R2[text search hits]
R1 --> C[merge / format]
R2 --> C
很多 RAG 文章只讲“向量检索多么语义化”,但真实系统里更重要的问题是:
如果向量检索坏了,系统会不会直接瘫掉?
SecurityClaw 在这里做对的一点,就是给 retrieve() 留了 fallback:
- 先做 kNN 搜索
- kNN 异常时退回关键词检索
为什么这个降级比精度更重要 #
因为生产系统真正致命的问题不是“召回差一点”,而是“整个知识层失效”。
一个能降级的检索系统,比一个只在 happy path 下表现漂亮的检索系统更值钱。
四、build_context 真正解决的是上下文预算问题 #
很多人把 RAG 的最后一步写成“把结果拼起来就行”。其实不是。
def build_context(query: str, k=5) -> str:
hits = retrieve(query, k)
lines = ["Relevant evidence:"]
for i, hit in enumerate(hits, 1):
lines.append(f"{i}. [{hit['category']}/{hit['source']}] {hit['text']}")
return "\n".join(lines)
这个函数表面上只是在格式化文本,但它实际上承担了三个控制任务:
- 限制召回数量:不是所有相关内容都能塞进 prompt
- 定义证据格式:模型看到的是摘要化证据,不是原始数据湖
- 压缩上下文预算:让检索参与决策,而不是淹没决策
这也是为什么我不喜欢把 RAG 叫“长期记忆” #
因为真正的长期记忆应该可恢复、可索引、可演化;而 build_context 的输出只是一次性的 prompt 证据片段。
更准确的理解是:
RAG 提供的是“临时注入的知识证据”,不是运行中的状态。
五、嵌入模型和聊天模型为什么必须分离 #
llm:
ollama_model: qwen2.5:7b-instruct-q4_K_M
ollama_embed_model: nomic-embed-text:latest
这不是“为了架构好看”,而是并发稳定性的需要。
如果不分离,会发生什么 #
- 聊天和嵌入抢同一模型资源
- 高并发下 embedding 请求卡死
- Agent 看起来像是“推理慢”,其实是知识层阻塞了
分离之后的好处 #
- 聊天模型负责 reasoning
- 轻量 embedding 模型负责召回
- 两者吞吐和成本模型不同,运行时更稳定
这个设计的本质,是把“推理成本”和“检索成本”从同一个瓶颈上拆开。
六、RAG 什么时候不该参与 #
这点非常重要,而且很多项目完全没讲。
RAG 不该在这些场景里强行参与:
6.1 问题是纯当前状态问题 #
例如:
- 当前这轮 plan 到哪一步了
- 当前工具刚返回了什么
- 当前权限是什么
这些属于 state,不属于 RAG。
6.2 问题不需要历史证据 #
例如:
- 一个确定性的 schema 校验
- 一个固定的权限判断
- 一个预算判断
这些应该由代码直接回答,而不是去检索。
6.3 检索成本高于收益 #
如果一次检索本身非常慢,或者返回内容高度噪音,强行注入只会污染 prompt。
所以更准确的原则是:
RAG 应该在“当前状态不足以支撑判断,但外部证据可能改变决策”时介入。
七、RAG 与 state / checkpoint 的边界 #
这三者最容易混:
| 层 | 作用 | 生命周期 |
|---|---|---|
| State | 当前这轮运行中的结构化状态 | 单次执行 |
| Checkpoint | 可恢复的会话状态 | 跨请求 / 跨重启 |
| RAG | 长期可检索证据库 | 长期积累 |
如果你把 RAG 结果直接塞进 checkpoint,问题会很快出现:
- checkpoint 变大
- 恢复变慢
- 旧检索证据污染新问题
- prompt 不断膨胀
SecurityClaw 把它们拆开,这一点是对的。RAG 应该保持为外部知识层,而不是运行状态的一部分。
八、这一篇真正要记住的框架 #
你可以把 RAG 压缩成这条链:
保存证据
-> 检索证据
-> 过滤证据
-> 格式化证据
-> 注入当前决策
其中最关键的不是“有没有向量数据库”,而是两点:
- 检索失败时能否降级
- 上下文注入是否受预算控制
这两点决定了 RAG 是系统能力,还是系统负担。
九、验证命令 #
pytest tests/test_rag.py tests/test_rag_fallback.py -v
如果要手动验证语义检索与降级:
python run_mock.py