游乐游手机版
首页/AI教程/文章详情

动手搭建AI Agent:RAG基础教程

时间:2026-05-29 07:46
系列文章: 1 动手做一个AIAgent - 简易框架搭建 2 动手做一个AIAgent - LiteLLM 3 动手做一个AIAgent - 流式输出与视觉支持 4 动手做一个AIAgent - MCP 5 动手做一个AIAgent - SKILLS 6 动手做一个AIAgent -
系列文章: 1. 动手做一个AIAgent - 简易框架搭建 2. 动手做一个AIAgent - LiteLLM 3. 动手做一个AIAgent - 流式输出与视觉支持 4. 动手做一个AIAgent - MCP 5. 动手做一个AIAgent - SKILLS 6. 动手做一个AIAgent - RAG基础 LLM是基于某个时间点之前的数据集训练出来的,这意味着它无法得知这个时间点之后发生的事,也无法触及训练集(比如企业内部的文档)之外的信息。 当然,你可以把所有需要告知的文档一股脑塞进 prompt 里,再向它提问。但问题在于,文档本身可能很庞大。一方面,这样会大量消耗 token;另一方面,过多的冗余信息也会让 LLM 产生更多幻觉。还有研究表明,当 LLM 处理长上下文时,会出现只记得开头和结尾,却丢失中间内容的情况——也就是“Lost in the Middle”现象。

什么是RAG

所以,我们需要尽可能地只将与当前对话相关的信息提供给 LLM,最好别用无关信息干扰它的文本生成。换个思路来说,就是让 LLM 在需要的时候,自己去数据库里查询与当前对话相关的内容。这就是 RAG(Retrieval-Augmented Generation),中文叫做检索增强生成。 举个例子,用户问“AI是什么?”我们不能只搜索包含“AI”这两个字母的文档,那些语义相近的文档,比如“深度学习”、“人工智能”之类的最好也能一并检索出来。这意味着传统的数据库在这类场景下行不通,我们需要用到向量数据库。 可以把向量理解成坐标——将一份文本根据语义转换成一个多维空间里的坐标。语义相近的文本,转换出来的坐标也就相近。 这样一来,RAG 的简要流程就比较好理解了: 将文档按某种规则分好块,计算出每一块的向量(也就是坐标)保存到向量数据库。当用户发出请求时,把用户的问题也计算出向量,然后在向量数据库里查询出与问题向量最接近的几个文档分块,最后把这几块内容交给 LLM 去生成最终回答。 这里截取了部分阿里巴巴 Java 开发手册的内容,下面就以它为例,介绍如何使用 Python 实现整个 RAG 流程。

离线阶段

离线阶段对应知识库的构建(文档加载 → 分块 → 向量化 → 存储),它是检索阶段的前置准备。

分块

首先需要对文档进行分块,常见的做法有这几种: - **固定大小分块**:按固定的字符数分割。 - **递归字符分块**:设定一个块的大小,先按段落分割,如果段落太长再分割出句子,如果句子还是太长再分割出单词。 - **按语义分块**:在文本主题发生变化的地方进行分割。 这里的文档是按小点列举出来的,所以可以把每一个小点分割成一个块: ```python def load_rag_chunks(doc_path: Path) -> List[str]: """读取文档,按全角左方括号「【」切分为 RAG 分片。首段为第一个「【」之前的正文(如章节标题);其后每段以「【」开头,对应一条规约条目。""" path = Path(doc_path) text = path.read_text(encoding="utf-8") parts = text.split("【") chunks: List[str] = [] head = parts[0].strip() if head: chunks.append(head) for body in parts[1:]: chunk = ("【" + body).strip() if chunk: chunks.append(chunk) return chunks ``` 分出来的块是这样的(以横线展示分割): ``` 1.1 命名风格 ------------------ 【强制】代码中的命名均不能以下划线或美元符号开始,也不能以下划线或美元符号结束。反例:_name / __name / O b j e c t / n a m e / n a m e O b j e c t / name_ / name O b j e c t /name/​name / O b j e c t$ ------------------ 【强制】代码中的命名严禁使用拼音与英文混合的方式,更不允许直接使用中文的方式。说明:正确的英文拼写和语法可以让阅读者易于理解,避免歧义。注意,即使纯拼音命名方式也要避免采用。正例:alibaba / taobao / youku / hangzhou 等国际通用的名称,可视同英文。反例:DaZhePromotion [打折] / getPingfenByName() [评分] / int 某变量 = 3 ------------------ 【强制】类名使用 UpperCamelCase 风格,必须遵从驼峰形式,但以下情形例外:DO / BO /DTO / VO / AO正例:MarcoPolo / UserDO / XmlService / TcpUdpDeal / TaPromotion反例:macroPolo / UserDo / XMLService / TCPUDPDeal / TAPromotion ------------------ ... ```

向量化

接下来,需要用 Embedding 模型去计算每个分块的向量。 ```python from sentence_transformers import SentenceTransformer embedding_model = SentenceTransformer("BAAI/bge-small-zh-v1.5") def embed_chunk(chunk: str) -> List[float]: embedding = embedding_model.encode(chunk, normalize_embeddings=True) return embedding.tolist() embeddings = {chunk: embed_chunk(chunk) for chunk in chunks} ``` 第一次执行上面的脚本时,会自动从 Hugging Face 下载 `BAAI/bge-small-zh-v1.5` 模型,会有一段等待时间,下载完成后就会复用缓存。Hugging Face 上也有比较详细的用法说明。 模型选择可以参考 Hugging Face 的 MTEB(Massive Text Embedding Benchmark)排行榜: (此处为图片) 或者可以切换到 `Performance per Model Size` 气泡图,更直观地对比每个模型的性能、成本、文本长度等维度: (此处为图片) - 横轴:模型参数量(Number of Parameters),也就是 LLM 里常说的多少多少 B,参数量越大一般能力越强,但对内存、显存等硬件性能的要求也越高。 - 纵轴:得分(Mean Task),使用一系列测试集来测试模型得出的分数,分数越高代表这个模型的语义理解能力越强。 - 气泡大小:维度(Embedding Size),模型最终计算出的向量的维度,维度越高能保持的语义信息越多,但占用的资源也会越多。 - 气泡颜色:最大处理长度(Max Tokens),模型支持的文本长度上限,颜色越深能处理越长的文本。

保存到向量数据库

向量计算完成后,就可以将向量和它代表的文档关联起来,保存到向量数据库。 向量数据库的选择不少,但用于学习的话,还是推荐使用轻量级的开源数据库 Chroma: ```python import chromadb chromadb_client = chromadb.EphemeralClient() chromadb_collection = chromadb_client.get_or_create_collection(name="default") def sa ve_embeddings(embeddings: dict[str, List[float]]) -> None: for chunk, embedding in embeddings.items(): chromadb_collection.add( documents=[chunk], embeddings=[embedding], ids=[chunk] ) sa ve_embeddings(embeddings) ``` 这里的 `EphemeralClient` 是一种用于临时、内存中操作的客户端模式,数据仅存在于内存中,程序退出或会话终止后数据会自动丢失。如果需要保存下来下次继续使用,可以用 `PersistentClient`: ```python chromadb.PersistentClient(path=str(cache_dir)) ```

在线阶段

在线阶段对应的是用户提问后的实时检索与生成。虽然在文章开头我们简单把这个过程总结为:从向量数据库查询与用户问题最接近的几个文档分块,提供给 LLM 去生成最终回答,但实际这个阶段还有不少需要处理的细节。

Query 改写

用户的问题通常会存在模糊、口语化、不完整或歧义等问题。例如,用户可能在讨论 Java 里的枚举,然后问了一句“它的命名有什么样的规范?”。 这时就需要 LLM 根据上下文,把查询语句改写成“枚举类的命名有什么样的规范?”。

召回

得到了比较准确的查询语句之后,就需要去向量数据库里检索了,这个步骤叫做召回: ```python def retrieve(query: str, top_k: int) -> List[str]: query_embedding = embed_chunk(query) results = chromadb_collection.query( query_embeddings=[query_embedding], n_results=top_k ) return results['documents'][0] retrieved_chunks = retrieve(query, 10) ``` 由于向量数据库里会有海量的文档,召回的重点在于速度,需要尽量快地找到相近的文档。 在速度优先的情况下,就可能出现精度缺失,容易漏掉一些内容。所以通常会从向量数据库里召回比较多的文档,例如 20 份或者更多。这里的召回数量通常用 `Top-K` 参数表示。

重排

召回步骤得到了比较多的文档,而且它们的排序也可能有问题——最相关的文档反而被放到了中间位置。所以需要对召回的文档做更精细化的检查,将它们按相关性重新排序,这个步骤叫做重排: ```python cross_encoder = CrossEncoder('cross-encoder/mmarco-mMiniLMv2-L12-H384-v1') def rerank(query: str, retrieved_chunks: List[str], top_k: int) -> List[str]: pairs = [(query, chunk) for chunk in retrieved_chunks] scores = cross_encoder.predict(pairs) scored_chunks = list(zip(retrieved_chunks, scores)) scored_chunks.sort(key=lambda x: x[1], reverse=True) return [chunk for chunk, _ in scored_chunks][:top_k] reranked_chunks = rerank(query, retrieved_chunks, 3) ``` 召回是广撒网,重排就是对召回的数据精加工。重排部分也有专门的 rerank 模型,可以在排行榜里筛选 rerank 模型来做选择: (此处为图片)

生成

这时候,可以在重排结果里选择相关性最高的 3 个或 5 个,连同用户的问题一起发给 LLM,来做最终结果的生成。可以给这个 RAG 搭一个简单的 MCP Server,导入到我们的 AI Agent 里面: ```json { "llm": {...}, "mcp": { ... "coding-standards-rag-server": { "command": "python3", "args": ["./mcps/rag_demo/coding_standards_rag_server.py"] }, ... } } ``` 然后就可以和它对话,用 RAG 查询编码规范了: ``` 请输入(Ctrl+C 退出): java的枚举类和c语言的枚举有什么差别 ..................... ...省略这部分内容... ==================== 请输入(Ctrl+C 退出): 根据我们的编码规范,它的命名有什么要求 ..................... ...思考中... 用户问的是关于枚举类命名的编码规范要求。我需要使用RAG检索编码规范的相关内容来回答这个问题。我应该搜索关于枚举命名的内容。 我来帮您查找编码规范中关于枚举命名的要求。 调用工具: MCP$coding-standards-rag-server$get_result, 参数: {'query': '枚举命名 enum 命名规范'} 工具调用结果: ["{...}"] 根据编码规范的查询结果,我找到了关于枚举命名的要求。让我整理一下关键信息: 1. 枚举类名建议带上 Enum 后缀 2. 枚举成员名称需要全大写,单词间用下划线隔开 从查询结果来看,这是完整的枚举命名规范要求,我可以直接向用户提供这些信息。 根据我们的编码规范,枚举类的命名有明确要求: ...中间部分省略... ...思考结束... 根据我们的编码规范,枚举类的命名有明确要求: ## 枚举命名规范 ### 1. 枚举类名 - **建议**带上 `Enum` 后缀 - 示例:`ProcessStatusEnum` ### 2. 枚举成员名称 - 必须**全大写** - 单词之间用**下划线**隔开 - 示例:`SUCCESS`、`UNKNOWN_REASON` ## 3. 完整示例 ```java public enum ProcessStatusEnum { SUCCESS, // 成功 FAILED, // 失败 UNKNOWN_REASON // 未知原因 } ``` 或者带有构造参数的枚举: ```java public enum DayEnum { MONDAY(1), TUESDAY(2), WEDNESDAY(3), THURSDAY(4), FRIDAY(5), SATURDAY(6), SUNDAY(7); private int value; DayEnum(int value) { this.value = value; } } ==================== ## 使用建议 规范还提到,如果变量值仅在一个范围内变化,且带有名称之外的扩展属性,应该定义为枚举类。例如上面的 `DayEnum` 中,数字就是扩展信息,表示星期几。这样命名可以提高代码的可读性和一致性,便于团队协作维护。 ``` 从上面可以看到,我们问了它两个问题: 1. Java 的枚举类和 C 语言的枚举有什么差别 2. 根据我们的编码规范,它的命名有什么要求 然后,从它调用 MCP 的日志可以看出,它自动联系上下文,把请求的 query 改写成了 `枚举命名 enum 命名规范`: ``` 调用工具: MCP$coding-standards-rag-server$get_result, 参数: {'query': '枚举命名 enum 命名规范'} ```
来源:https://juejin.cn/post/7621798739880132627
上一篇巧用AI工具提升承揽合同撰写效率指南 下一篇Flodo核心功能解析与实用指南
本站内容用于信息整理与展示,如有侵权或内容问题请及时联系处理。

相关推荐

补充同频道和同主题内容,方便继续浏览更多相关内容。

同类最新

继续查看同栏目最近更新的文章。

更多
2026年研发协同Top4流向式卡片任务流转工具实战选型指南
AI教程 · 2026-05-30

2026年研发协同Top4流向式卡片任务流转工具实战选型指南

在独立开发者与中小研发团队中,存在一个普遍却略显反常的现象:代码编写速度飞快,但产品上线却异常缓慢。 坦诚地说,许多开发者习惯将GitHub视为唯一战场——用Issues管理需求池,用Projects跟踪进度。起初尚可应付,但随着产品线扩张,那些静态列表中的任务、墙上的看板逐渐力不从心。非技术成员无

谷歌AI重塑电商购物传统SEO让位AIO
AI教程 · 2026-05-30

谷歌AI重塑电商购物传统SEO让位AIO

```html 你是否已经很久没有亲自“逛”过电商平台了?想买一件商品,打开应用,搜索关键词,翻阅评价,对比价格,加入购物车,填写地址,完成付款。整个流程走下来,快的十分钟,慢的能纠结一下午。但 Google 显然不这么认为——他们觉得,这套流程该被淘汰了。本周,Google 推出了 Univers

文旅内容结构化赋能景区GEO运营长期价值
AI教程 · 2026-05-30

文旅内容结构化赋能景区GEO运营长期价值

文旅内容创作者在日常运营中常面临一个挑战:内容更新频繁,但信息之间缺乏系统关联,如同孤岛。例如,一位专注“北京中轴线文化”的创作者,可能既发布过“鼓楼”的深度文章,也拍摄过“前门”的短视频,但这些优质内容彼此割裂,难以形成合力。 那么,如何打破这种内容断层?内容结构化的思路正被越来越多创作者采纳。所

IntelliJ IDEA 2026重大更新 开发体验全面升级
AI教程 · 2026-05-30

IntelliJ IDEA 2026重大更新 开发体验全面升级

JetBrains2026年IDE全面可访问性升级:修复Windows放大镜光标跟随问题,大幅增强LinuxOrca屏幕阅读器支持,显著改进Alt键与区域跳转的键盘导航,并创新探索代码音频反馈。这些优化惠及所有开发者,尤其提升残障用户操作体验,充分彰显技术包容性。

AI智能在线人脸识别测年龄我看起来像几岁
AI教程 · 2026-05-30

AI智能在线人脸识别测年龄我看起来像几岁

揭秘How Old Do I Look AI:年龄预测工具是什么你是否曾好奇过,在他人眼中你大约多少岁?如今,一款名为How Old Do I Look AI的工具正能为你解答这个疑惑。它背后搭载了成熟的AI系统,由专业团队精心研发:只需上传一张自拍照,系统便会通过分析面部皱纹走向、皮肤纹理、面部轮