Bonus-2 · RAG / Agent 全景实战手册
从向量库选型 → 检索策略 → Agent 框架 → tool calling,搭一条真能跑的 RAG/Agent 链路,并落地实测结果。技术栈:k3s + vLLM(Qwen2.5-3B,内网 ClusterIP) + Python 3.10 + ChromaDB。
0. 为什么 RAG/Agent 是 LLM 落地的"半壁江山"
裸调 LLM 的两个硬伤:
| 痛点 | 表现 | 解法 |
|---|---|---|
| 知识截止 | "Claude 4.X 啥时候发的?" 答不出来 | RAG: 检索外部知识喂进 context |
| 不能动手 | "查一下今天日历,然后发邮件" 做不到 | Agent + Tool calling: 让模型调函数 |
RAG = 检索增强生成 · Agent = 决策 + 工具循环。两者经常组合(Agent 里某个 tool 就是 RAG retriever)。
1. 向量库全景
1.1 主流方案对比
| 引擎 | 部署 | 语言 | 适用规模 | 索引 | 特点 |
|---|---|---|---|---|---|
| ChromaDB | embedded / server / cloud | Python 原生 | < 10M docs | HNSW | 最简,Python 用户友好,自带 embedding |
| Qdrant | server / cloud | Rust core | 10M-1B | HNSW + 量化 | 性能强,过滤表达式好用,gRPC + REST |
| Milvus | k8s 分布式 | C++ core | 1B+ | HNSW / IVF / DiskANN | 大厂级,水平扩展,运维重 |
| Weaviate | k8s / cloud | Go | 10M-100M | HNSW | 内置 generative module(自动 RAG) |
| pgvector | PostgreSQL 扩展 | PG plugin | < 10M | IVF / HNSW | 不引入新组件,跟业务表 join 友好 |
| FAISS | in-process | C++ + Python | 任意 | IVF / HNSW / PQ | Meta 出的"底层库",自己组装,无服务 |
| OpenSearch / Elasticsearch | server | Java | TB 级 | HNSW + 倒排 | 同时支持 BM25 + dense,hybrid retrieval 首选 |
| Vespa | server | Java + C++ | TB 级 | HNSW + ColBERT | 检索+排序一体,Yahoo/Spotify 在用 |
1.2 选型决策树
QPS < 100, docs < 100K → ChromaDB embedded
QPS < 1K, docs < 10M → Qdrant single node
QPS > 1K, docs > 100M → Milvus cluster / Vespa
已有 PostgreSQL,数据强一致 → pgvector
需要 hybrid (BM25 + dense) → OpenSearch / Vespa
完全离线 / 嵌入式 → FAISS in-process
1.3 本次实测选 ChromaDB:轻、快、零配置
pip install chromadb openai
# venv 约 480 MB,含 onnxruntime / numpy / protobuf / tokenizers / pydantic
2. Embedding 模型全景
| 模型 | 维度 | 语言 | 体积 | 备注 |
|---|---|---|---|---|
| all-MiniLM-L6-v2 | 384 | EN 为主 | 90 MB | ChromaDB 默认,onnx,中文质量差 |
| bge-large-zh-v1.5 | 1024 | 中文 | 1.3 GB | 智源,中文 retrieval 排行榜第一梯队 |
| bge-m3 | 1024 | 多语 | 2.3 GB | 同时输出 dense / sparse / colbert |
| m3e-base | 768 | 中英 | 410 MB | 国产,中英混合场景 |
| text-embedding-3-large | 3072 | 100+ 语言 | API | OpenAI, $0.13/1M tokens |
| voyage-3 | 1024 | 多语 | API | Anthropic 推荐,RAG 专精 |
| gte-Qwen2-7B-instruct | 4096 | 多语 | 14 GB | 大模型当 embedding,top tier,慢 |
中文 RAG 不要用 all-MiniLM,改用 bge-base-zh 起步。本次 demo 用默认 onnx 模型纯属图轻,代价是 retrieval 质量打折(见下文实测)。
3. 检索策略全景
3.1 单路检索
| 策略 | 原理 | 优势 | 短板 |
|---|---|---|---|
| Dense (semantic) | embedding cosine | 同义/改写鲁棒 | 字符精确匹配差(产品名/代码) |
| Sparse (BM25) | 词袋 + TF-IDF | 精确字符匹配,可解释 | 同义词盲区 |
3.2 多路混合
| 策略 | 实现 | 何时用 |
|---|---|---|
| Hybrid (dense + BM25) | RRF 融合 / 加权和 | 大多数生产场景默认 |
| HyDE (Hypothetical Document Embedding) | 先让 LLM 生成假答案,再用假答案的 embedding 检索 | query 太短/太抽象 |
| Multi-Query | LLM 重写 query 成 N 个,各自检索,合并 | query 模糊 / 多意图 |
| Step-back | LLM 把 query 抽象成更上层问题再检索 | 需要背景知识的复杂问题 |
| Parent-Document | child chunk 索引,命中后召回 parent 段 | 需要完整上下文(法律、医疗) |
3.3 重排(Re-ranking)
检索召回 top-N(N=50-100)后,用 cross-encoder 重新打分,取 top-K(K=3-10):
| Reranker | 类型 | 备注 |
|---|---|---|
bge-reranker-large | cross-encoder | 中文好,免费 |
cohere-rerank-v3 | API | 多语,$2/1k searches |
mxbai-rerank-large | cross-encoder | EN,SOTA |
ColBERT v2 | late interaction | 速度兼顾质量 |
3.4 分块策略 (Chunking)
| 策略 | 适用 |
|---|---|
| Fixed-size (e.g. 512 tokens, 50 overlap) | 大多数,简单可用 |
| Semantic (按句子 embedding 突变切) | 文档语义跳跃明显时 |
Recursive (按 \n\n → \n → 逐层切) | LangChain 默认,工程上稳 |
| Markdown header split | 技术文档,结构化文件 |
| Code-aware (AST) | 代码库 RAG |
4. 实测 1:ChromaDB + ONNX embedding + vLLM 端到端 RAG
4.1 完整 demo 代码
import chromadb
from openai import OpenAI
import time
DOCS = [
{"id": "k8s-pod", "text": "Pod 是 K8s 最小调度单元,包含 1 个或多个容器共享 network namespace 和 volume,生命周期一致。"},
{"id": "k8s-deploy","text": "Deployment 通过 ReplicaSet 间接管理 Pod 副本数,支持滚动升级(RollingUpdate)和 Recreate 两种策略。"},
{"id": "k8s-svc", "text": "Service 通过 selector 把 Pod 抽象成稳定的 ClusterIP / NodePort / LoadBalancer 服务,后端 Pod 死掉自动剔除。"},
{"id": "k8s-pvc", "text": "PVC 是用户对存储的声明式申请,绑定 PV 后通过 CSI 驱动挂载到 Pod。"},
{"id": "k8s-hpa", "text": "HPA 基于 metrics-server 提供的 CPU/Memory 或 custom metric 自动扩缩 Deployment 副本数。"},
{"id": "vllm-pa", "text": "vLLM 的 PagedAttention 把 KV cache 分页管理,显存碎片减少 4×,30 并发请求下吞吐提升 24×。"},
{"id": "vllm-cb", "text": "Continuous Batching 让 vLLM 在 iteration level 调度,新请求随时加入,不需要等 batch 凑齐。"},
{"id": "mig", "text": "NVIDIA MIG (Multi-Instance GPU) 在 A100/A800/H100 上把单卡硬件切成最多 7 个独立 instance,显存和 SM 都隔离。"},
]
client = chromadb.PersistentClient(path="./chroma_data")
collection = client.get_or_create_collection(name="k8s_facts")
collection.upsert(ids=[d["id"] for d in DOCS], documents=[d["text"] for d in DOCS])
llm = OpenAI(base_url="http://vllm-3b.vllm.svc.cluster.local:8000/v1", api_key="not-needed")
def rag_chat(question, top_k=3):
res = collection.query(query_texts=[question], n_results=top_k)
docs = res["documents"][0]; ids = res["ids"][0]
context = "\n".join(f"[{i+1}] ({did}) {d}" for i, (did, d) in enumerate(zip(ids, docs)))
prompt = f"请基于以下知识回答(末尾标注源 [1][2][3]):\n知识:\n{context}\n\n问题: {question}\n\n答复:"
t0 = time.time()
resp = llm.chat.completions.create(
model="qwen2.5-3b",
messages=[{"role": "user", "content": prompt}],
max_tokens=200, temperature=0.3,
)
return resp.choices[0].message.content, time.time()-t0, ids
4.2 实测输出
检索质量
Q: K8s 的 Pod 是什么?
top-1 [k8s-pod] (dist=0.605): Pod 是 K8s 最小调度单元... 命中
top-2 [k8s-pvc] (dist=0.990): PVC 是用户对存储的声明式申请...
Q: vLLM 为什么吞吐高?
top-1 [k8s-pvc] (dist=1.214): PVC... 错配
top-2 [vllm-pa] (dist=1.270): vLLM PagedAttention... (才到 top-2)
Q: A800 GPU 怎么切片?
top-1 [mig] (dist=0.645): NVIDIA MIG... 命中
top-2 [k8s-hpa] (dist=1.321): HPA...
Q: 怎么让 K8s 自动扩缩?
top-1 [k8s-pod] (dist=0.805): Pod... 应该召 hpa
top-2 [k8s-deploy] (dist=1.174): Deployment...
真实问题:vLLM 为什么吞吐高 和 怎么让 K8s 自动扩缩 两题的 top-1 都错了。原因清晰:
ChromaDB 默认 embedding 是
all-MiniLM-L6-v2,英文模型。中文 query 走它会丢语义。生产中文 RAG 必须换bge-base-zh或bge-m3。
但好在 RAG 是 top-K 多路召回,top_k=3 时正确文档基本都进了 context,LLM 仍能从中挑出真正相关的写进答案。
端到端 RAG 生成质量(top_k=3)
| Q | LLM 答案(节选) | 来源命中 | 耗时 |
|---|---|---|---|
| K8s 的 Pod 是什么? | "Kubernetes 最小的调度单元。一个 Pod 可以包含一个或多个容器,共享网络命名空间和存储卷..." | k8s-pod, k8s-pvc, k8s-deploy | 1919 ms |
| vLLM 为什么吞吐高? | "PagedAttention 将 KV cache 分页管理,显存碎片减少,30 并发吞吐提升 24 倍" | k8s-pvc, vllm-pa, k8s-pod | 1596 ms |
| A800 GPU 怎么切片? | "通过 NVIDIA MIG 技术被切分成最多 7 个独立的 instance..." | mig, k8s-hpa, vllm-pa | 1572 ms |
| 怎么让 K8s 自动扩缩? | "通过 Deployment + HPA + ReplicaSet..."(部分跑题) | k8s-pod, k8s-deploy, k8s-pvc | 4101 ms |
关键观察:即使 top-1 embedding 错,只要 top-K 把对的文档捞进来,LLM 端的"语义裁判"还是能挑出正确事实。这就是 RAG 容错性强于纯 embedding 的核心原因。
4.3 优化方向
- 换中文 embedding:
pip install sentence-transformers+bge-base-zh-v1.5 - 加 reranker:
bge-reranker-base把 top-20 重排成 top-3 - Hybrid search:同时跑 BM25 + dense,RRF 融合
- HyDE:让 LLM 先生成假答案,用假答案做检索
5. Agent 框架全景
| 框架 | 立场 | 学习曲线 | 适用 |
|---|---|---|---|
| LangChain | 一切皆 Chain,生态最大 | 高(API 不稳) | 快速搭原型,Python 后端 |
| LangGraph | LangChain 的状态机版本 | 中 | 复杂多步 agent(有循环/分支) |
| LlamaIndex | RAG 主线,从 doc loader 到 query engine | 中 | 文档 QA / 数据应用 |
| AutoGen | 多 agent 对话(MS Research) | 中 | 角色协作型(Coder + Reviewer) |
| CrewAI | 类 AutoGen,UX 更简 | 低 | 业务流程编排 |
| smolagents | HuggingFace 出品,Code-Agent 范式 | 低 | 想让模型直接写 Python 跑 |
| OpenAI Agents SDK | OpenAI 官方,2025 推出 | 低 | 用 OpenAI/Anthropic API 时首选 |
| Pydantic-AI | 强类型 schema 优先 | 中 | 生产代码,严结构化输出 |
| MCP (Anthropic) | Tool 服务的协议标准,跨 agent 复用 | 中 | Tool 资源跨产品/团队共享 |
5.1 选型建议
快速原型 + Python 后端 → LangChain / LangGraph
文档 QA 主线 → LlamaIndex
多角色协作场景 → AutoGen / CrewAI
生产 API 强 schema → Pydantic-AI / OpenAI Agents SDK
跨工具复用 / 标准化 → 把工具暴露成 MCP server
"我就是想让它写 Python" → smolagents
5.2 ReAct = Agent 的最小核心算法
核心 prompt 范式:
Thought: 思考下一步
Action: tool_name(args)
Observation: <工具返回>
Thought: 综合判断
Action: ...
...
Final Answer: <最终回答>
循环:LLM 输出 → 正则解析 Action → 执行 tool → 把 Observation 追加到 prompt → 再 call LLM,直到 Final Answer 出现。 这是所有 Agent 框架的"底层逻辑"——拆任何 LangChain agent,本质都是这个 loop。
6. Tool Calling 三种实现方式
| 方式 | 模型要求 | 优势 | 劣势 |
|---|---|---|---|
| A. OpenAI Function Calling (推荐) | 模型微调过,如 Qwen2.5 / GPT-4o / Claude | 模型直出结构化 JSON,稳 | 依赖模型支持 + 推理框架开启 --enable-auto-tool-choice |
| B. ReAct prompting | 任意模型 | 0 改动,任意推理后端 | 解析脆弱,需要正则 + 重试 |
| C. Code-as-action (smolagents) | 任意模型 | 表达力强,可以组合 | 需要沙箱执行,安全风险 |
6.1 vLLM 开启原生 function calling
vllm serve Qwen/Qwen2.5-3B-Instruct \
--enable-auto-tool-choice \
--tool-call-parser hermes
本次实测条件:线上的 vllm-3b 启动时没开这两个 flag,所以走 B. ReAct 方案。
7. 实测 2:ReAct Agent + 3 工具
7.1 工具定义
def tool_calculator(expr): # safe eval, 限制在 math 模块
def tool_datetime(_): # 当前时间
def tool_kb_lookup(key): # 小型 KB: vllm / mig / hpa
TOOLS = {"calculator": ..., "datetime": ..., "kb_lookup": ...}
7.2 ReAct loop 核心
ACTION_RE = re.compile(r"Action:\s*(\w+)\(([^)]*)\)")
FINAL_RE = re.compile(r"Final Answer:\s*(.+)", re.DOTALL)
while step < max_steps:
resp = llm.chat.completions.create(..., stop=["Observation:"])
out = resp.choices[0].message.content
if FINAL_RE.search(out): return final
m = ACTION_RE.search(out)
obs = TOOLS[m.group(1)](m.group(2))
messages.append({"role": "assistant", "content": out})
messages.append({"role": "user", "content": f"Observation: {obs}"})
7.3 实测三道题
Case 1: 时间查询(1 步通)
Q: 现在几点?今天星期几?
[Step 1] Thought: 需要获取当前日期时间来回答问题。
Action: datetime()
Obs: 2026-05-27 01:19:01 Wednesday
[Step 2] Final Answer: 当前日期时间是 2026 年 5 月 27 日星期三。
Case 2: 数学计算(踩坑 + 自愈 4 步)
Q: 帮我算一下 sqrt(144) + 3 * pow(2, 5) 是多少?
[Step 1] Action: calculator(sqrt(144) + 3 * pow(2, 5))
Obs: ERROR: '(' was never closed
真实 bug:正则 [^)]*) 在第一个 ) 就停了,args 变成 'sqrt(144'
[Step 2] Action: kb_lookup(vllm)
模型一开始走偏,试图 KB 查表
[Step 3] Thought: 知识库没有,我手动算: sqrt(144)=12, 3*pow(2,5)=96
Action: calculator(12 + 96)
Obs: 108
[Step 4] Final Answer: 108
真实 Agent 现场:工具正则有 bug → 模型靠多步分解绕过 → 最终答对。生产里要么修正则(用括号配对栈),要么用 OpenAI function calling 让模型直出 JSON args 避免歧义。
Case 3: 知识库查询(1 步通)
Q: vLLM 是什么? 用知识库查一下.
[Step 1] Action: kb_lookup(vllm)
Obs: vLLM 是 UC Berkeley 提出的高吞吐 LLM 推理引擎...
[Step 2] Final Answer: vLLM 是 UC Berkeley 提出的高吞吐 LLM 推理引擎...
8. 生产化要点 Checklist
8.1 RAG 上线前必做
- [ ] embedding 模型选对语言(中文别用 MiniLM)
- [ ] chunking 至少试 3 种(固定 / recursive / semantic),A/B
- [ ] top_k 调到 5-10,加 reranker 截到 3
- [ ] 加 citation(prompt 让模型标 [1][2][3]),否则不可追溯
- [ ] 检索失败兜底(没召到 → 走 LLM 原生回答 + 标注"未基于知识库")
- [ ] chunk 去重(同一段被切多个有重叠时)
- [ ] 多租户隔离(metadata filter / 单 tenant 单 collection)
- [ ] embedding 漂移监控(模型升级要回填重建索引)
8.2 Agent 上线前必做
- [ ] 超时 + max_steps 上限(防止死循环烧 token)
- [ ] 工具沙箱(
calculator一定要限制 eval scope) - [ ] 失败重试 + 降级(工具异常时模型要能 graceful 处理)
- [ ] 审计日志(每个 Action / Observation 全打)
- [ ] 成本上限(单会话 token 数封顶)
- [ ] 危险工具二次确认(发邮件/付钱/删文件 → 必须 human-in-the-loop)
- [ ] MCP 化:把工具暴露成 MCP server,不同 agent 复用同一套工具
8.3 性能要点
| 优化 | 效果 |
|---|---|
| Async batch retrieval | 10+ query 并发,QPS ×8 |
缓存 embedding(hash(query) → vec) | 重复查询零开销 |
缓存 LLM 结果(hash(prompt) → answer) | 常见 Q 直接返回 |
| Stream output | TTFT 从 2s → 200ms,UX 飞跃 |
| KV cache reuse(prefix sharing) | system prompt 复用,prefill 时间 ×0.3 |
9. 一句话总结
RAG 解决"模型不知道" · Agent 解决"模型不能动手" · MCP 让工具跨 agent 复用 · 底层全是 ReAct loop。
选向量库看规模,选 embedding 看语言,选 reranker 看预算,选 agent 框架看团队习惯。
面试常见题
Q1: 完整 RAG 链路有哪些环节? A: 五段式:(1) embedding 把 doc 切片向量化入库 → (2) vector DB 存 + 建 HNSW/IVF 索引 → (3) retrieve(query embedding 召回 top-N,可叠 BM25 hybrid + rerank 收到 top-K) → (4) augment(拼 prompt,带 citation 占位) → (5) generate(LLM 基于 context 出答案)。每段都可能成为瓶颈:embedding 选错语言、chunking 切碎语义、检索缺 hybrid、prompt 没让模型引用来源。
Q2: 为什么 hybrid retrieval 通常比纯 vector 好? A: dense embedding 强在同义/改写召回,弱在字符精确匹配(产品名、错误码、人名、代码符号常被语义"模糊化")。BM25 反之,精确字符强、同义弱。两者用 RRF(reciprocal rank fusion)或加权和合并,覆盖语义召回 + 精确召回两个失效模式。生产中文 RAG 几乎是 BM25 + dense 默认起步。
Q3: ReAct vs Plan-and-Execute Agent 的区别? A: ReAct 是单步循环(Thought → Action → Observation → Thought → ...),每一步看上一步结果再决策,擅长简单 lookup 类任务,但长链路容易跑偏、token 浪费。Plan-and-Execute 先让 LLM 一次性出完整 plan,再逐步执行(执行器可以是更便宜的小模型),适合多步可预测任务(报告生成、数据 ETL)。混合方案:LangGraph 用状态机,允许 plan 之后中途回到 react 重新规划。
Q4: Tool Calling 三种实现方式各自适用场景? A: (1) OpenAI Function Calling:模型微调过 + 推理框架支持(vLLM --enable-auto-tool-choice),JSON args 稳,生产首选。(2) ReAct prompting:任意模型可用,0 改动,适合本地小模型或推理框架没开 function calling 时的兜底,代价是正则解析脆。(3) Code-as-action:模型直接生成 Python 代码,组合表达力最强,适合数据分析/科学计算场景,但必须沙箱(Docker / E2B / WASM)否则危险。
Q5: 生产 RAG 怎么评估,核心指标有哪些? A: 分检索层和生成层。检索层:hit rate @ K(正确 doc 是否在 top-K)、MRR(mean reciprocal rank,看正确 doc 的平均排名)、nDCG(考虑多个相关 doc 的排序质量)。生成层:faithfulness(答案是否忠于 context,不幻觉)、answer relevance(答案是否切题)、context precision(召回的 context 是否真的有用)。工具:RAGAS、TruLens、DeepEval。线下用 golden set 跑离线评估,线上用 LLM-as-judge + 用户反馈点赞/点踩做持续监控。