
第一章:RAG 基础知识
1.1 什么是 RAG
RAG(Retrieval-Augmented Generation,检索增强生成)是一种将信息检索与大语言模型生成结合的技术架构。核心理念是:不让 LLM 凭空编答案,而是先检索外部知识库中相关片段,再基于片段生成回答。
举一个典型场景:用户问"D0 入催率怎么算?",传统 LLM 可能回答"大概是逾期订单除以总订单",口径模糊甚至不准确。RAG 会先检索到指标字典中的定义"D0 订单入催 = 1 - dpd0_repay / dpd0_cnt",再基于这个片段生成准确答案。答案可溯源,口径可查,显著降低幻觉问题。
1.2 核心组件
RAG 系统包含五个核心环节:
① 分块(Chunking):将长文档切分为适合检索的小片段。分块质量直接决定检索效果。常见策略有按标题层级、按段落、按固定字符数切分。chunk size 太大会引入噪声且可能超出演示上下文上限,太小会丢失上下文关联。
② 向量化(Embedding):将文本映射为稠密向量,使语义相近的文本在向量空间中距离接近。Embedding 是向量检索的基础,决定了检索的语义理解能力。常用模型包括 OpenAI text-embedding-3(效果好但收费)、BGE-small-zh(国产开源,CPU 可跑)、m3e-base(中文友好)等。
③ 检索(Retrieval):根据用户 query 从向量数据库或索引中召回最相关的 top_k 片段。检索是 RAG 的核心,决定了能给 LLM 提供什么知识。
| 方式 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| BM25 | 词频统计 + 逆文档频率 | 不需要 Embedding 模型,本地可跑,对专有名词效果好 | 无法处理同义词/语义相似 |
| 向量检索 | 语义向量相似度 | 理解语义,支持同义词匹配 | 需要 Embedding 模型和向量数据库 |
| 混合检索 | BM25 + 向量融合 | 两者兼顾,召回更全 | 实现复杂度高 |
④ Prompt 组装:将检索结果、用户问题、系统指令组装成 LLM 的输入。Prompt 设计决定模型能否按预期工作。常见策略包括:明确回答格式(先结论后依据)、加置信度约束(证据不足时回答"不知道")、加边界规则(明确哪些问题不能答)、加引用格式(答案需附带来源)。
⑤ LLM 生成:基于组装好的 Prompt 输出最终答案。生成模型可本地部署(qwen/LLaMA/ChatGLM 系列)或调用 API(GPT-4/Claude 系列)。
1.3 分块策略
分块策略的选择需要根据文档结构和业务场景调整:
| 策略 | 原理 | 适用场景 | 潜在问题 |
|---|---|---|---|
| 按标题层级 | 以 # 标题为边界切分 |
Markdown、结构化文档 | 单个标题可能没有实质内容 |
| 按段落 | 以段落换行符切分 | 段落明确的文档 | 段落可能过长 |
| 固定大小 | 按字符数硬切(如 900char) | 通用场景 | 可能切断语义单元 |
| 递归切分 | 递归地按段落→句子切 | 通用场景 | 实现复杂 |
在实际落地中,分块质量比算法选择更重要。常见优化手段包括:过滤无意义的纯标题碎片、清洗 Markdown 噪音(列表符号、序号、代码标记)、过滤过短内容(少于 40 字符)、按内容密度过滤(标题行占比超过 70% 的片段)。
1.4 检索策略选择
向量检索和 BM25 并不是非此即彼的选择,实际系统中可以同时保留,通过配置切换。
BM25 的适用场景:专有名词密集的业务文档(如"入催率"、“利差”、“件均”),对精确匹配的召回效果不输向量检索,且不需要额外的 Embedding 模型,适合资源受限的环境。缺点是对同义词、口语化表达(如"借多少钱" vs “合同金额”)召回效果差。
向量检索的适用场景:语义理解要求高的场景,如同义词丰富的知识库、多语言文档、需要语义相似度排序的情况。代价是需要额外的 Embedding 模型和向量数据库资源。
更优的方案是混合检索 + Rerank:第一阶段用 BM25 或向量检索快速召回 top_20,第二阶段用 CrossEncoder 或 BGE-Rerank 精排取 top_4。两阶段检索能显著提升相关性。
1.5 评估方法
RAG 评估通常从检索和生成两个维度分开进行。
业界主流评估框架
| 框架 | 核心思路 |
|---|---|
| RAGAS | 从 Faithfulness、Answer Relevance、Context Relevance 三个维度用 LLM 评分 |
| ARES | 引入 LLM 作为评判,结合参考答案为答案打分 |
| Trulens | 提供检索和生成双侧的可量化追踪仪表盘 |
| LangSmith | 支持线上 RAG 的 trace 和评估,支持人工标注集成 |
检索侧评估
| 指标 | 含义 | 评估目标 |
|---|---|---|
| Recall@K | top_k 结果中包含正确答案的比例 | 召回是否全面 |
| MRR | 正确答案在结果中的平均排名倒数 | 最佳结果是否靠前 |
| NDCG | 综合相关性加权的排名评估 | 综合排名质量 |
生成侧评估
| 方法 | 说明 | 局限性 |
|---|---|---|
| LLM-as-Judge | 用 GPT-4/Claude 评判生成答案的质量 | Prompt 模板驱动,主观性较强 |
| BLEU/ROUGE | 基于 n-gram 重合度的自动评分 | 无法判断事实正确性 |
| 人工评估 | 随机抽样,人工判断答案正确性 | 成本高,难以规模化 |
生成侧评估的难点在于:自动化指标(BLEU/ROUGE)只能衡量文本重合度,无法判断答案的事实正确性和语义一致性。因此在精度要求高的业务场景中,人工抽检仍是不可或缺的环节。
我的 RAG 评估实践
结合业界方法和实际资源限制,我采用以下策略:
- 检索侧:使用 Recall@K 和 MRR 量化检索质量,定期抽检 top_6 结果的相关性
- 生成侧:以人工评估为主,每次迭代随机抽取 20-30 条真实 query 进行评测
- 端到端:在实际会议查询场景中收集 Bad Case,持续优化系统
1.6 优化方向
RAG 是一个需要持续迭代的系统,常见优化方向包括:
- 检索侧:Query 改写(让 query 更适合检索)、Query 扩展(同义词补充)、混合检索 + Rerank 精排
- 生成侧:Self-RAG 让模型判断是否需要检索、Corrective-RAG 检查幻觉并重新检索
- 知识库侧:知识分层(按重要性/置信度分级)、增量索引(新文档自动更新)
第二章:背景与动机
2.1 业务背景
我是一名信贷行业的商业分析师,日常工作中依赖一套多 Agent 系统进行数据分析和报告产出。其中 asset agent 是使用频率最高的 Agent,主要服务于两类核心场景:
- 会议数据查询:在会议过程中,快速查询特定维度的资产数据(如某渠道、某包体、某客群的入催率、利差等)
- 报告生成:日常生成日报、周报时,需要反复查询数据并按固定口径进行汇总
2.2 asset agent 的能力与演进
asset agent 负责资产数据分析的全维度问题——放款规模、入催率、利差、渠道分布、包体表现……每次会议中当我需要回答"本周前置新客的 D0 入催率是多少?较上周环比如何?“这类问题时,asset agent 需要从数据库或 Excel 文件中拉取数据、筛选维度、计算指标、输出结论。
此前的实现方案是通过调用资产分析的 Skills 完成——Skills 中预先封装了所有维度和指标的定义、计算口径和分析逻辑,agent 根据用户问题调用对应 Skill 执行分析。
然而,随业务发展,Skills 中积累的维度和指标持续扩展:
| 维度/指标 | 持续增加的内容 |
|---|---|
| 渠道 | Facebook / Google / TikTok / Organic / 非投,投放包 7 个 |
| 包体 | 7 个投放包 + APK 包 |
| 客群 | 老客 / 新客(纯新/非纯新)/ 老客多笔 / 新客多笔 |
| 产品 | 前置/后置 × 单笔/多笔 |
| 指标 | D-1/D0 入催、D0 利差、至今利差、件均、坏账率… |
| RG 等级 | A / B / C / D / E / F / G |
Skills 方案的局限性逐渐显现:
- 可读性差:Skills 中的口径以代码形式存在,调用方只能获得结果,无法直接查看分析过程
- 维护成本高:维度和指标增加时需同步更新 Skills 代码,跨场景复用困难
- 扩展性有限:新增分析维度或临时性探索时,Skills 结构难以灵活扩展
因此,需要引入 RAG 知识库作为 Skills 的补充:将描述性知识(定义、口径、SOP)迁移至知识库,由 RAG 提供问答式检索服务;Skills 保留结构化的计算逻辑,两者各司其职。
2.3 知识管理的压力
即使引入知识库,知识本身的管理也面临挑战。资产分析知识分散在多个位置:
- 飞书文档:指标字典、维度定义、分析 SOP
- 代码注释:部分口径逻辑内嵌于 Python 脚本
- DM 对话:临时讨论出的口径约定,仅存于对话记录
问题随之暴露:
- 口径不统一:同一术语在不同文档中定义不一致,agent 回答时可能产生偏差
- 更新不同步:代码逻辑变更后,文档未同步更新,导致知识不一致
- 复用性差:口径知识难以快速迁移至报告自动生成等新场景
- 维护成本高:理解 agent 的能力边界需要较长时间
资产分析的维度和指标将持续增加,知识管理的复杂度将持续上升。我需要建立一套系统化的知识管理机制,这便是 RAG + 知识向量库的原始驱动力。
2.4 为什么是 RAG
业界知识管理的主流技术已转向 RAG。AI Native 应用和企业知识库问答场景中,RAG 几乎成为默认技术选型:
| 方案 | 业界现状 |
|---|---|
| 纯文档搜索 | 上一代方案,缺乏语义理解能力 |
| 硬编码规则 | 维护成本高,难以规模化 |
| 纯 LLM 记忆 | 上下文窗口有限,大规模知识无法承载 |
| RAG | 业界主流方案,适用于几乎所有知识问答场景 |
RAG 的核心能力——自然语言问答 + 知识可更新 + 答案可溯源——恰好匹配会议快速查询和报告生成的核心需求。
作为 AI 应用开发者,掌握 RAG 也是必备技能。我有明确的学习目标:分块策略、检索链路、Prompt 约束、效果评估,唯有亲手实践方能真正掌握;并在 asset agent 的真实场景中验证 RAG 对工作效率的实际提升。
第三章:搭建系统流程
3.1 搭建流程总览
环境准备(安装依赖/Ollama/拉取模型)
↓
配置管理(编写 config.yaml)
↓
知识库建设(整理 md 文件)
↓
索引构建(python build_index_v2.py)
↓
服务启动(python app.py)
↓
验证测试(/health → /search → /ask)
3.2 环境准备
Python 依赖:fastapi uvicorn pydantic pyyaml jieba rank_bm25 chromadb requests
Ollama 模型:用 ollama pull qwen2.5:0.5b。一个模型两用——既做 Embedding 也做生成,节省资源。
3.3 项目结构
asset_rag/
├── app.py # FastAPI 服务,核心推理逻辑
├── build_index_v2.py # 索引构建脚本
├── config.yaml # 所有可配置参数
├── prompt.md # 系统 Prompt 模板
├── index.json # BM25 索引(构建后生成)
└── chroma_db/ # 向量数据库(启用向量模式后)
asset_knowledge_base/ # 知识库,与服务分离
├── 01_指标口径/
├── 02_维度定义/
├── 03_数据字典/
└── 04_分析SOP/
设计思路:知识库独立维护,重建索引后服务自动感知新知识。
3.4 配置管理
所有参数集中在 config.yaml:
ollama_base_url: http://127.0.0.1:11434
ollama_model: qwen2.5:0.5b
top_k: 6
max_context_chunks: 4
chunk_max_chars: 900
use_vector: false # 切换检索模式
3.5 核心代码:服务入口
app = FastAPI(title='asset-local-rag')
@app.post('/ask')
def ask(req: AskRequest):
hits = retrieve(req.question, req.top_k or TOP_K)
answer = call_ollama(req.question, hits)
return {'answer': answer, 'citations': hits}
3.6 核心代码:统一检索入口
def retrieve(query: str, top_k: int):
if USE_VECTOR:
return retrieve_vector(query, top_k) # Chroma 向量
else:
return retrieve_bm25(query, top_k) # BM25
BM25 检索:用 rank_bm25 + jieba 分词,对专有名词召回效果好,不需要 Embedding 模型。
向量检索:use_vector: true 时启用,用 Ollama 做 Embedding,存在 Chroma 中。
3.7 核心代码:Prompt 组装
def call_ollama(question: str, contexts: list):
context_text = '\n\n'.join([
f"[来源: {c['path']}]\n{c['text']}"
for c in contexts[:MAX_CONTEXT]
])
prompt = f"{PROMPT}\n\n知识片段:\n{context_text}\n\n用户问题:{question}"
resp = requests.post(f'{OLLAMA_URL}/api/generate',
json={'model': OLLAMA_MODEL, 'prompt': prompt})
return resp.json()['response'].strip()
设计要点:最多取 4 个 chunk 给 LLM,每个 chunk 标注来源。
3.8 索引构建
python3 build_index_v2.py
build_index_v2.py 读取所有 md 文件,按 # 标题切分,过滤无意义碎片和 Markdown 噪音,输出 index.json。
3.9 启动与验证
python3 app.py
# 健康检查
curl http://127.0.0.1:8787/health
# RAG 问答测试
curl -X POST http://127.0.0.1:8787/ask \
-H "Content-Type: application/json" \
-d '{"question": "D0入催率怎么算?"}'
第四章:知识库建设
4.1 设计原则
知识库是 RAG 的"大脑”,质量直接决定检索效果。遵循三个原则:
- 边界明确:第一版只覆盖 asset_data 相关内容
- 结构分层:按"口径 → 维度 → 数据字典 → 分析 SOP"分层组织
- 格式规范:统一 Markdown 格式,每个概念独占一个
#标题
4.2 目录结构
asset_knowledge_base/
├── 01_指标口径/指标字典.md # 指标定义和计算公式
├── 02_维度定义/维度字典.md # 渠道、客群、包体等维度分类
├── 03_数据字典/asset_data.md # 资产数据表字段说明
└── 04_分析SOP/ # 放款下降/首逾恶化/利差恶化等分析思路
分层作用:口径层(定义"是什么")、数据层(定义"有什么字段")、应用层(定义"怎么做")。
4.3 md 写作规范
每个概念独占一个 # 标题,避免被切散到不同 chunk:
## D0 订单入催率
定义:到期日当天仍未还款的订单占比。
公式:D0订单入催 = 1 - dpd0_repay / dpd0_cnt
避免纯列表式写作,列表项要有解释说明。
4.4 知识边界划定
| 能回答 | 不能回答 |
|---|---|
| 放款规模、件均、入催率、利差 | 通过率、审批链路 |
| 渠道/包体/客群维度定义 | 催收策略 |
| 指标口径、数据字段说明 | operation_data |
边界控制在 Prompt 层实现,知识库本身不做特殊处理。
4.5 维护流程
整理 md 文件 → 提交到 asset_knowledge_base/ → 执行 build_index_v2.py → 服务自动感知
第五章:分块策略迭代
5.1 分块的重要性
分块是 RAG 中最容易被忽视但影响极大的环节。分块质量直接决定检索回来的内容是否完整。
5.2 v1:按标题 + 字符硬切
按 # 标题分割,900 char 以内直接作为 chunk,超过则按段落累积切分。
v1 暴露的问题:
| 问题 | 现象 | 影响 |
|---|---|---|
| 纯标题碎片 | # 二级标题 没有正文也被当成 chunk |
检索回来是空内容 |
| Markdown 噪音 | 列表符号、序号干扰 Embedding | 向量质量下降 |
| 上下文割裂 | 定义和公式被切成两半 | 检索回来不完整 |
5.3 v2:过滤 + 清洗
过滤无意义碎片(少于 40 字符、标题行占比超过 70% 的不要)和清洗 Markdown 噪音(去掉列表符号、序号、代码标记)后再建索引。
def is_meaningful_chunk(text: str) -> bool:
if len(stripped) < 40: return False
if title_lines / len(lines) > 0.7: return False
return True
5.4 反思
900 char 硬编码在某些场景仍有局限:公式和说明被切到不同 chunk、表格被切碎、跨章节引用失效。v2 是资源受限下的实用解,不是最优解。
第六章:检索链路设计
6.1 BM25 检索
BM25 是稀疏检索算法,本质是 TF-IDF 的升级版:对 query 分词 → 计算 TF → 引入 IDF 惩罚常见词 → 文档长度归一化。
优点: 不需要 Embedding 模型,本地可跑;对专有名词(“D0入催”、“件均”)召回效果好。
缺点: 只能字面匹配,同义词(“件均” vs “平均合同金额”)无法召回。
6.2 向量检索
通过 config.yaml 的 use_vector: true/false 切换。用 Ollama 做 Embedding,存在 Chroma 中。
缺点: 需要额外的 Embedding 模型和向量数据库资源,实测在 2 vCPU / 3.8GB 机器上速度较慢。
6.3 统一检索入口
def retrieve(query: str, top_k: int):
if USE_VECTOR:
return retrieve_vector(query, top_k)
else:
return retrieve_bm25(query, top_k)
两种模式通过配置切换,对上层透明。
6.4 检索结果示例
{
"id": "01_指标口径/指标字典.md#3",
"path": "01_指标口径/指标字典.md",
"text": "## D0 订单入催\n\n定义:到期日当天仍未还款...\n公式:D0订单入催 = 1 - dpd0_repay / dpd0_cnt",
"score": 8.67
}
score 是 BM25 相关性得分,可用于判断置信度。
第七章:Prompt 工程与安全约束
7.1 系统 Prompt 设计
Prompt 设计决定模型能否按预期工作,遵循三个原则:
- 范围白名单:明确列出能回答的主题
- 拒绝黑名单:明确列出不能回答的领域和标准回复话术
- 不确定性约束:证据不足时不能编造,明确说"不知道"
7.2 我的 Prompt
你是 asset agent 的本地 RAG 问答模型。
严格规则:
- 只能基于提供的知识片段回答
- 只能围绕 nigeria_asset.asset_data 第一版范围回答
- 涉及通过率、催收执行等超出范围的问题,必须回答"当前知识库不支持该问题"
- 不要编造字段、口径、结论
7.3 用户 Prompt 组装
context_text = '\n\n'.join([
f"[来源: {c['path']}]\n{c['text']}"
for c in contexts[:MAX_CONTEXT] # 最多取 4 个 chunk
])
prompt = f"{PROMPT}\n\n知识片段:\n{context_text}\n\n用户问题:{question}"
关键设计: MAX_CONTEXT = 4 避免超过模型上下文上限,每个 chunk 标注来源方便溯源。
7.4 边界测试效果
| 问题 | 预期行为 | 实际 |
|---|---|---|
| “D0 入催怎么算?” | 基于指标字典回答 | ✅ |
| “本周通过率是多少?” | “知识库不支持” | ✅ |
| “哪些订单逾期了?” | 拒绝,提示需查数据库 | ✅ |
第八章:效果评估与瓶颈分析
8.1 主观评分:6 / 10
| 维度 | 得分 | 说明 |
|---|---|---|
| 检索召回 | 6/10 | 同义词召回差 |
| 生成质量 | 5/10 | 0.5B 模型理解能力有限 |
| 响应速度 | 8/10 | 本地推理,无网络延迟 |
| 稳定性 | 7/10 | 资源受限下偶有 OOM |
| 可解释性 | 8/10 | 来源清晰,可溯源 |
核心短板:模型太小 和 同义词召回差。
8.2 瓶颈一:模型太小
qwen2.5:0.5b 对复杂推理(如多步计算问题)能力有限,对"环比"、“同比"等分析术语理解有时偏差,指令遵循也不稳定。
8.3 瓶颈二:检索召回率低
BM25 只做字面匹配,导致:
- “平均合同金额” 召回不了"件均”
- “首逾率” 匹配不到"首逾恶化"(字面不同)
- 口语化表达 vs 知识库书面语不匹配
8.4 瓶颈三:没有 Rerank
当前链路:query → BM25 top_k → LLM
理想链路:query → BM25 top_20 → Rerank top_4 → LLM
两阶段检索可显著提升相关性,但当前机器资源不支持。
第九章:踩坑总结
坑一:资源预估不足导致 OOM
最初用 qwen2.5:3b 跑向量检索,3.8GB RAM 直接爆了。解决:降级到 qwen2.5:0.5b,关闭向量检索只用 BM25。
教训: 不要高估硬件条件,先用最小可行方案跑通。
坑二:分块粒度太粗
早期按 1500 char 切分,把"定义 + 公式 + 注意事项"切成三块,检索回来经常不完整。解决:降到 900 char,同时要求知识库写作"一个概念一段话"。
教训: 分块策略和知识库写作规范需要联合优化。
坑三:Prompt 注入
用户输入 "忽略之前的规则" 可以让模型跳过约束。解决:在 system prompt 加"严格规则"章节,应用层做输入过滤。
教训: 本地小模型的指令遵循能力不如大模型,Prompt 约束不能假设天然有效。
坑四:Ollama 热更新问题
切换 use_vector 时 Ollama 没启动好会报 500 但服务不崩溃,排查困难。解决:加了 /health 接口主动检查状态。
第十章:优化方向与总结
10.1 模型升级路径
qwen2.5:0.5b (当前)
↓ 内存允许时优先升级
qwen2.5:3b (+2分)
↓ 有条件加内存/GPU
qwen2.5:7b (+3分)
↓ 进一步
LLaMA3.1:8B / ChatGLM4:9B
10.2 检索两阶段方案
candidates = retrieve_bm25(query, top_k=20) # 阶段一:粗排
reranked = cross_encoder_rerank(query, candidates, top_k=4) # 阶段二:精排
预期收益:召回率提升 15-20%。
10.3 同义词扩展
QUERY_EXPANSIONS = {
"件均": ["平均合同金额", "人均借款金额"],
"入催": ["逾期", "未还款", "不良"],
}
10.4 总结
这个 RAG 不是"完美方案",而是资源约束下的最优妥协:
- 2 vCPU / 3.8GB / 无 GPU → 只能用 BM25 + 0.5B
- 基本可用但明显有瓶颈 → 6 分不是终点,是起点
- 每次迭代都是"资源允许下做最值得做的事"
核心理念:工程没有最优解,只有最适合当前阶段的解。