从零实现一个基于ReAct+RAG的智能问答Agent,我把踩过的坑都整理好了

好的,我来帮你写一篇CSDN风格的技术博客,大概1.5万字,通俗易懂,帮你巩固这个项目。

先规划好了,这篇博客我会用CSDN的风格来写,口语化、有干货、带踩坑记录,帮你彻底巩固这个项目。现在开始写。

文件太大一次写入超时了,我分批来写。先看看文件有没有部分生成。

我分成两部分来写,这样不容易超时。先写前半部分。

写入工具老是超时,我换个方式,用Python脚本来写文件。先写第一部分。

让我先试试简单的命令,看看环境是不是正常。

可能是内容太大导致超时。我先写个小文件测试一下。

让我试试其他工具能不能正常工作。

看来文件写入工具暂时有点问题,没关系,我直接把博客内容写在回复里,你可以复制保存。这篇博客我会用CSDN的风格来写,口语化、有干货、带踩坑记录,帮你彻底巩固这个项目。


从零实现一个基于ReAct+RAG的智能问答Agent,我把踩过的坑都整理好了

前言

大家好,我是一名正在学习大模型应用开发的研究生。最近跟着导师做了一个扫地机器人智能问答Agent的项目,用的是ReAct+RAG的技术方案。

说实话,一开始我是懵的——什么ReAct、什么RAG、什么LangChain,听着都很高大上,但真的上手的时候,连环境都搭了半天。

现在项目终于跑通了,我想着把整个过程整理成一篇博客,一方面是给自己做个总结,另一方面也希望能帮到和我一样刚入门的同学。毕竟我踩过的坑,你们就别再踩了。

这篇博客大概1.5万字,我会从最基础的概念讲起,到环境搭建、项目结构、核心代码、踩坑记录,一条龙服务。如果你也是刚接触大模型应用开发,或者想做一个自己的Agent项目,相信这篇文章会对你有帮助。

你能从这篇文章学到什么:

  • 搞懂RAG和ReAct到底是啥(用人话讲,不整虚的)

  • 知道LangChain这个框架到底能干啥

  • 从零搭一个能跑的智能问答Agent

  • 了解Agent项目的代码结构怎么设计

  • 避开我踩过的那些坑

废话不多说,咱们开始。


一、先搞懂:这项目到底是干啥的?

在讲技术之前,先搞明白一个最基本的问题:我们做这个项目,到底是为了解决啥问题?

1.1 背景:智能客服的痛点

现在扫地机器人越来越普及了,谁家还没个扫地机器人呢对吧?但用户买回去之后,问题也跟着来了:

  • “边刷怎么换啊?”

  • “机器卡住了怎么办?”

  • “故障代码E10是什么意思?”

  • “怎么连接WiFi?”

这些问题说难不难,但量大啊。如果全靠人工客服来答,那成本可就高了。而且人工客服还有个问题——水平参差不齐,同一个问题,不同客服可能给的答案都不一样。

那有人说了,用关键词匹配的自动客服不行吗?

还真不太行。用户的问法千奇百怪,比如同样是问边刷怎么换,有人说"边刷怎么换",有人说"怎么更换边刷",有人说"边刷坏了咋整",关键词匹配很容易漏。而且关键词匹配只能答预设好的问题,稍微复杂一点、需要推理的,它就不行了。

1.2 大模型的出现,带来了新的可能

大模型火了之后,大家就想:能不能用大模型来做智能客服?

大模型确实厉害,能理解自然语言,回答也很流畅。但直接用大模型做客服,又有新的问题:

第一个问题:幻觉。

就是大模型会一本正经地胡说八道。比如你问它"故障代码E10是什么意思",它可能会给你编一个听起来很合理但完全错误的答案。用户要是照着做,机器可能就真坏了。

第二个问题:专业知识不够。

大模型是通用模型,什么都知道一点,但什么都不精。扫地机器人这种垂直领域的专业知识,它可能就答不好。

第三个问题:知识更新难。

出了新机型、有了新功能,总不能每次都重新训练大模型吧?那成本也太高了。

1.3 我们的解决方案:ReAct + RAG

那怎么办呢?业界的主流方案就是RAG加上Agent

简单说:

  • RAG(检索增强生成):让大模型回答问题之前,先去知识库查一下相关资料,然后照着资料来回答。这样就不会瞎编了,而且更新知识只要更新知识库就行。

  • ReAct(推理+行动):让大模型像人一样思考——先想想该干啥,然后调用工具去干,干完了看看结果,再决定下一步。这样就能处理更复杂的问题。

我们这个项目,就是把这两个技术结合起来,做一个针对扫地机器人领域的智能问答Agent。

说起来好像挺简单,但真的做起来,细节还是挺多的。别急,后面我会一点点拆开来讲。


二、核心概念扫盲:RAG、ReAct、LangChain都是啥?

在动手写代码之前,先把几个核心概念搞明白。不然后面看代码你会一头雾水。

2.1 RAG:让大模型"开卷考试"

RAG的全称是Retrieval-Augmented Generation,翻译过来叫"检索增强生成"。

名字听着挺玄乎,其实原理特别简单。

你就想啊,我们上学考试的时候,闭卷考试容易考砸,开卷考试就好多了——因为可以查资料嘛。

大模型也是一样。直接让它回答问题,相当于闭卷考试,它只能靠自己"记忆"里的知识,就容易记错、瞎编。

RAG就是让大模型"开卷考试"——回答问题之前,先去知识库(也就是"教材")里查一下相关的内容,然后照着查出来的内容来回答。

这样一来,答案就有依据了,不容易胡说八道。而且知识库更新也方便,有新的知识了直接加进去就行,不用重新训练模型。

RAG的工作流程

RAG分为两个阶段:建库阶段查询阶段

建库阶段(离线做,做一次就行):

  1. 文档加载:把PDF、Word、TXT这些文档读进来

  2. 文本分块:把长文档切成一小块一小块的(比如每块300字)。为什么要切?因为太长了模型塞不下,而且检索的时候也不精准

  3. 向量化:把每一块文本都转成一个向量(就是一串数字)。这个向量能代表这段文本的"意思",意思相近的文本,向量也相近

  4. 存入向量数据库:把这些向量存起来,方便后面快速查找

查询阶段(用户提问时实时做):

  1. 问题向量化:把用户的问题也转成向量

  2. 相似度检索:拿着问题的向量,去向量数据库里找最像的几个文本块(就是找最相关的内容)

  3. 拼接提示词:把找到的相关内容和用户的问题拼在一起,组成一个完整的提示词

  4. 生成答案:把提示词发给大模型,让大模型照着资料来回答

是不是挺简单的?核心就是"先查再答"四个字。

2.2 ReAct:让大模型学会"思考+行动"

ReAct的全称是Reasoning + Acting,也就是推理加行动。

这个也很好理解。你就想我们人是怎么解决问题的:

比如你想做番茄炒蛋,你不会一下子就把菜做好,而是一步一步来:

  • 先想想:我得先看看冰箱里有没有食材

  • 然后行动:打开冰箱看看

  • 然后观察:哦,有番茄但没鸡蛋

  • 再想想:那得去买鸡蛋

  • 再行动:下楼去超市买

  • 再观察:买回来了

  • 再想想:现在可以开始做了

  • ...

你看,这就是一个"思考→行动→观察→再思考"的循环。

ReAct就是让大模型也这么干。它不是一下子就给出答案,而是一步一步来:

  • Thought(思考):分析当前的情况,决定下一步该做什么

  • Action(行动):调用对应的工具去执行

  • Observation(观察):获取工具执行的结果

然后把观察结果再反馈给模型,继续思考下一步,直到任务完成。

ReAct有啥好处?

第一,能处理复杂任务。 有些问题不是一步就能解决的,需要多步推理、调用多个工具,ReAct就能搞定。

第二,可解释性强。 每一步模型是怎么想的、做了什么、得到了什么结果,都看得清清楚楚,不是一个黑盒。出了问题也容易排查。

第三,能力容易扩展。 想加新能力?加个新工具就行了,不用重新训练模型。

我们这个项目里,ReAct Agent就是"大脑",负责思考和决策;RAG就是其中一个"工具",负责查知识库。

2.3 LangChain:大模型应用开发的"瑞士军刀"

LangChain是一个专门用来做大模型应用开发的框架。

你可以把它理解成一个"工具箱",里面有各种各样现成的工具和组件,你直接拿来用就行,不用从零开始写。

比如:

  • 想加载文档?它有各种Document Loader

  • 想分块?它有各种Text Splitter

  • 想接大模型?它封装了各种LLM的接口

  • 想做RAG?它有现成的Chain

  • 想做Agent?它也有Agent框架

  • 想接向量数据库?它也支持各种主流的向量库

总之,LangChain把大模型应用开发中常用的功能都封装好了,你只需要调用它的API,就能快速搭出一个应用。

当然,LangChain也不是完美的,它封装得比较深,有时候出了问题不好排查。但对于新手来说,用它来入门还是挺香的,能让你快速看到效果,建立信心。

2.4 几个概念的关系

最后再梳理一下这几个概念的关系,别搞混了:

  • RAG:一种技术方法,“先查再答”

  • ReAct:一种Agent框架,"思考→行动→观察"循环

  • LangChain:一个开发框架,里面有RAG和ReAct的实现

  • 向量数据库:用来存向量的,是RAG的基础设施

我们这个项目,就是用LangChain这个框架,实现了一个基于ReAct的Agent,这个Agent可以调用RAG工具来回答问题。

好了,概念就讲到这里。下面咱们进入实操环节。


三、环境搭建:手把手教你把环境搭起来

理论讲完了,咱们开始动手。第一步就是搭环境。

别小看环境搭建,我当初在这一步踩了不少坑。下面我把完整的步骤写出来,你跟着做就行。

3.1 整体环境清单

先列一下我们需要装的东西:

  • Python 3.9+

  • Ollama(用来本地跑大模型)

  • 项目依赖的Python库

  • (可选)Git

3.2 安装Python

这个应该不用我多说了吧?做Python开发的电脑上基本都有。

注意版本要3.9以上,建议用3.10或者3.11,比较稳定。

装的时候Windows用户记得勾选"Add Python to PATH",不然命令行里找不到。

验证一下:

python --version

能输出版本号就OK。

3.3 安装Ollama

Ollama是一个本地部署大模型的工具,特别好用,一行命令就能把模型跑起来。

下载安装:
去官网(ollama.com)下载对应系统的安装包,双击安装就行,没啥难度。

验证安装:
打开命令行,输入:

ollama --version

能输出版本号就说明装好了。

下载模型:
我们这个项目用两个模型:

  • 大模型:deepseek-r1:1.5b(1.5B参数,比较小,普通电脑就能跑)

  • 向量模型:bge-m3:latest(用来生成向量的)

拉取模型:

ollama pull deepseek-r1:1.5b
ollama pull bge-m3:latest

拉取需要一点时间,取决于你的网速,耐心等一下。

验证模型:
拉完之后可以测试一下:

ollama run deepseek-r1:1.5b

能进入对话界面就说明没问题。输入/exit退出。

💡 踩坑提醒
如果你的电脑配置比较低,1.5B的模型跑起来都费劲,可以试试更小的,比如qwen2:0.5b。
另外,如果有NVIDIA显卡,一定要装CUDA,Ollama会自动用GPU加速,速度会快很多。

3.4 创建虚拟环境(强烈建议)

虽然不是必须的,但我强烈建议你用虚拟环境。每个项目一个独立的Python环境,互不干扰,不会因为版本问题打架。

创建虚拟环境:
在项目根目录下打开命令行,输入:

python -m venv .venv

这会在当前目录下创建一个.venv文件夹。

激活虚拟环境:
Windows下:

.venv\Scripts\activate

Mac/Linux下:

source .venv/bin/activate

激活之后,命令行前面会出现(.venv)的提示,说明已经在虚拟环境里了。

💡 小技巧
后面所有的pip install和运行命令,都要先激活虚拟环境再执行。别搞混了。

3.5 安装项目依赖

激活虚拟环境之后,安装依赖:

pip install -r requirements.txt

如果下载慢,可以用国内镜像源:

pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple

💡 踩坑提醒
如果某个库安装失败,先看错误信息。大概率是缺少系统依赖,或者网络问题。
实在不行,可以试试单独安装那个失败的库,有时候批量装会出问题,单独装就好了。

3.6 准备数据

项目的data文件夹里应该有一些示例数据,比如"扫地机器人100问.pdf"之类的。

如果没有的话,你自己准备一些知识库文档放进去也行,支持PDF、Word、TXT这些格式。

第一次运行的时候,系统会自动从data文件夹加载文档,构建向量数据库。这个过程可能需要一点时间,取决于文档大小。

3.7 运行项目

一切准备就绪,就可以跑项目了。项目提供了三种运行方式,选一个你喜欢的。

方式一:Flask API服务

python app.py

启动后监听8002端口,可以用Postman或者curl测试。

方式二:Gradio Web界面

python app_web_gradio.py

启动后会显示一个本地地址,浏览器打开就能看到交互界面,简单直观。

方式三:Streamlit Web界面

streamlit run app_web_streamlit.py

Streamlit的界面更丰富一些,功能也更多。

💡 新手建议
建议用Gradio或者Streamlit,有界面,直观,容易看到效果。Flask API适合后端调用。


四、项目结构解析:每个文件夹、每个文件都是干啥的?

环境搭好了,先别急着跑代码。咱们先把项目结构搞清楚,知道每个文件是干啥的,后面看代码才不会晕。

4.1 整体目录结构

我把项目的目录结构整理了一下,大概是这样的:

ZST_code/
├── app.py                 # Flask入口文件
├── app_web_gradio.py      # Gradio界面入口
├── app_web_streamlit.py   # Streamlit界面入口
├── requirements.txt       # 依赖清单
├── Agent/                 # Agent层(大脑)
│   ├── ReAct.py           # ReAct Agent核心
│   └── Action.py          # Action数据模型
├── Tools/                 # 工具层(手)
│   ├── Tools.py           # 工具注册和管理
│   ├── RagQATool.py       # RAG问答工具
│   ├── VecStore.py        # 向量数据库管理
│   ├── ReportGenerationTool.py  # 报告生成工具
│   ├── ExtractInfo.py     # 信息提取
│   └── FetchFromOtherSys.py     # 对接其他系统
├── Models/                # 模型层(底子)
│   └── Factory.py         # 模型工厂
├── Configs/               # 配置文件
│   └── config.yml         # 主配置
├── prompts/               # 提示词
│   ├── main.txt           # 主提示词
│   └── report_prompt.txt  # 报告生成提示词
├── data/                  # 原始数据
│   └── 扫地机器人100问.pdf
├── Utils/                 # 工具函数
│   ├── FileReader.py      # 文件读取
│   └── PrintHandler.py    # 打印处理
├── Chroma/                # 向量数据库(自动生成)
├── Eval/                  # 评估相关
├── Tests/                 # 测试
└── Results/               # 结果输出

看着文件挺多的,其实核心的就那么几个。咱们一层一层来看。

4.2 分层架构设计

这个项目采用的是分层架构,从上到下一共五层:

  1. 用户交互层:就是那几个app开头的文件,负责和用户打交道

  2. Agent层:Agent文件夹,是系统的"大脑",负责思考和决策

  3. 工具层:Tools文件夹,是系统的"手",负责具体干活

  4. 模型层:Models文件夹,是系统的"底子",提供大模型和向量模型能力

  5. 数据层:data文件夹,存放原始的知识库文档

为什么要分层?因为这样职责清晰,每层只干自己的事。以后想改哪一层,不会影响其他层。比如想换个Web框架,只改用户交互层就行,Agent层完全不用动。

这是一种很经典的架构设计思想,不光是AI项目,普通的Web项目也是这么设计的。

4.3 核心文件简介

我挑几个最核心的文件,简单说一下它们是干啥的:

文件 作用 重要程度
Agent/ReAct.py ReAct Agent的核心实现,Agent怎么思考、怎么调用工具,都在这里 ⭐⭐⭐⭐⭐
Agent/Action.py Action的数据模型,定义了工具调用的格式 ⭐⭐⭐
Tools/Tools.py 工具注册和管理,有哪些工具、怎么调用,都在这里 ⭐⭐⭐⭐
Tools/RagQATool.py RAG问答工具的实现 ⭐⭐⭐⭐
Tools/VecStore.py 向量数据库的管理,建库、加载、检索 ⭐⭐⭐⭐
Models/Factory.py 模型工厂,创建和管理大模型、向量模型 ⭐⭐⭐
app.py Flask入口,API服务 ⭐⭐
Configs/config.yml 配置文件 ⭐⭐

后面我会挑最重要的几个文件,逐行来讲代码。


五、核心代码精讲:彻底搞懂每一行(上)

终于到了最核心的部分——代码精讲。

我会挑几个最核心的文件,一行一行地讲。别怕,我会用大白话讲,保证你能听懂。

5.1 Action.py:先定义"行动"的格式

我们先从最简单的开始——Action.py。这个文件虽然短,但很重要,它定义了Agent调用工具的格式。

from pydantic import BaseModel, Field

class Action(BaseModel):
    tool_name: str = Field(description="工具名称")
    args: dict = Field(description="工具参数,字典格式")

就这么几行。啥意思呢?

这是用Pydantic定义了一个数据模型,叫Action。它有两个字段:

  • tool_name:工具的名字,字符串类型

  • args:工具的参数,字典格式

比如,Agent想调用RAG问答工具,问"扫地机器人卡住了怎么办",那Action就是这样的:

{
    "tool_name": "rag_qa_tool",
    "args": {
        "query": "扫地机器人卡住了怎么办"
    }
}

为什么要定义这个?因为Agent的输出是文本,我们需要把它解析成结构化的数据,这样才能知道它想调用哪个工具、传什么参数。

Pydantic是一个很有用的库,它可以:

  • 定义数据结构

  • 自动做类型验证

  • 生成JSON Schema(可以用来告诉模型输出格式)

后面我们会看到,这个Action类不光是用来存数据的,它的Schema还会被放进提示词里,告诉模型应该输出什么格式。

5.2 ReAct.py:Agent的大脑,核心中的核心

接下来是重头戏——ReAct.py。这是整个Agent的核心,Agent怎么思考、怎么调用工具,都在这里。

这个文件稍微有点长,我分段来讲。

5.2.1 导入依赖
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import PydanticOutputParser, OutputFixingParser
from langchain_core.runnables import RunnablePassthrough
from langchain.tools.base import StructuredTool
from Models.Factory import llm
from Agent.Action import Action
from typing import List

导入了一堆东西,我们一个个说:

  • ChatPromptTemplate:聊天提示词模板,用来构建提示词

  • PydanticOutputParser:Pydantic输出解析器,把模型输出的文本解析成Action对象

  • OutputFixingParser:输出修复解析器,解析失败的时候自动让模型修正

  • RunnablePassthrough:这个是LCEL里的,简单说就是"透传",输入啥输出啥

  • StructuredTool:结构化工具,用来把普通函数包装成Agent能调用的工具

  • llm:从Models.Factory导入的大模型实例

  • Action:刚才讲的Action数据模型

  • List:类型提示用的

5.2.2 ReActAgent类
class ReActAgent:
    def __init__(self, tools: List[StructuredTool]):
        self.tools = tools
        self.tool_names = [tool.name for tool in tools]
        self.tool_descriptions = "\n".join([f"{tool.name}: {tool.description}" for tool in tools])
        
        self.parser = PydanticOutputParser(pydantic_object=Action)
        self.fixing_parser = OutputFixingParser.from_llm(parser=self.parser, llm=llm)

这是ReActAgent的初始化方法。接收一个tools参数,就是Agent可以调用的工具列表。

我们看看它做了啥:

  1. 把工具列表存起来

  2. 提取所有工具的名字,存成一个列表

  3. 把工具的名字和描述拼成一个字符串,后面要放进提示词里

  4. 创建一个PydanticOutputParser,用来解析模型的输出

  5. 创建一个OutputFixingParser,用llm来修复解析错误

这里重点说一下OutputFixingParser。

为什么需要这个?因为小模型经常输出不规范的JSON,比如少个括号、多个逗号、或者外面包了一堆解释性文字。如果直接用PydanticOutputParser解析,很容易失败。

OutputFixingParser的作用就是:如果解析失败了,它会把错误信息和原始输出一起再发给模型,让模型自己修正。这样成功率会高很多。

💡 我的体会
这个OutputFixingParser真的是神器。一开始我没用它,Agent的输出十次有八次解析失败,我都快崩溃了。后来加上这个,成功率一下子就上去了。
大模型应用开发,容错真的太重要了。你永远不知道模型会输出什么奇奇怪怪的东西。

5.2.3 构建提示词
def build_prompt(self):
        prompt = ChatPromptTemplate.from_messages([
            ("system", """你是一个扫地机器人领域的智能助手。
你可以使用以下工具来帮助回答用户问题:

{tool_descriptions}

请按照以下格式输出:
首先思考你需要做什么,然后决定调用哪个工具。

输出格式:
```json
{format_instructions}

注意:

  1. 只能调用上述工具,不要调用不存在的工具

  2. 每次只调用一个工具

  3. 工具名称必须是 [{tool_names}] 中的一个

用户问题:{query}

请输出你的思考和行动:“”"),
])
return prompt


这个方法是构建提示词的。提示词是Agent的"灵魂",写得好不好,直接影响Agent的表现。

我们来分析一下这个提示词的结构:

**1. 角色设定**

你是一个扫地机器人领域的智能助手。

开头先给模型设定一个角色。这是提示词工程的基本操作——给模型一个明确的身份,它的表现会更稳定。

**2. 工具说明**

你可以使用以下工具来帮助回答用户问题:

{tool_descriptions}

告诉模型有哪些工具可用,以及每个工具是干什么的。这部分是Agent能正确选择工具的关键。

tool_descriptions是动态生成的,每个工具一行,格式是"工具名: 工具描述"。

**3. 格式要求**

请按照以下格式输出:
首先思考你需要做什么,然后决定调用哪个工具。

输出格式:

{format_instructions}
告诉模型应该输出什么格式。format_instructions是PydanticOutputParser自动生成的,它会告诉模型JSON应该有哪些字段、每个字段是什么类型。

用```json代码块包裹,模型更容易理解"这部分是结构化输出",我们后续提取JSON也更容易。

**4. 约束条件**

注意:

  1. 只能调用上述工具,不要调用不存在的工具

  2. 每次只调用一个工具

  3. 工具名称必须是 [{tool_names}] 中的一个

这部分是"规则强调",反复告诉模型哪些能做、哪些不能做。

你可能会问:刚才不是已经说过有哪些工具了吗,为什么还要再列一遍工具名称?

因为大模型有时候会"健忘",或者注意力不够集中。把重要的规则重复强调,能提高遵守规则的概率。特别是第三条,把工具名称再列一遍,相当于给模型一个"白名单",减少它乱调用工具的概率。

**5. 用户问题**

用户问题:{query}

把用户的问题放进去。

**6. 结尾引导**

请输出你的思考和行动:

最后引导模型开始输出。明确告诉模型"现在该你输出了",能减少模型输出无关内容的概率。

> 💡 **提示词优化建议**:
> 这个提示词已经不错了,但还可以优化。比如可以加几个Few-Shot示例(就是给几个正确的输入输出例子),小模型的表现会好很多。
> 另外,还可以引导模型一步步思考,比如"在决定调用工具之前,请先思考:用户的问题是什么类型?需要调用工具吗?应该调用哪个工具?"

#### 5.2.4 构建Chain

```python
    def build_chain(self):
        prompt = self.build_prompt()
        
        chain = (
            {
                "query": RunnablePassthrough(),
                "tool_descriptions": lambda x: self.tool_descriptions,
                "tool_names": lambda x: ", ".join(self.tool_names),
                "format_instructions": lambda x: self.parser.get_format_instructions(),
            }
            | prompt
            | llm
            | self.fixing_parser
        )
        
        return chain

这个方法是构建Chain的,用的是LangChain的LCEL语法。

可能刚接触LCEL的同学会觉得有点晕,别慌,我给你拆解一下。

LCEL(LangChain Expression Language)是LangChain的一种表达式语言,用管道符|把各个组件串起来,就像Linux的管道一样。

我们从左到右看:

第一部分:输入处理

{
    "query": RunnablePassthrough(),
    "tool_descriptions": lambda x: self.tool_descriptions,
    "tool_names": lambda x: ", ".join(self.tool_names),
    "format_instructions": lambda x: self.parser.get_format_instructions(),
}

这是一个字典,用来准备提示词需要的各个变量。

  • query: RunnablePassthrough():把输入直接透传给query变量。简单说就是用户输入啥,query就是啥。

  • tool_descriptions: lambda x: self.tool_descriptions:把工具描述放进去

  • tool_names: lambda x: ", ".join(self.tool_names):把工具名用逗号连起来

  • format_instructions: lambda x: self.parser.get_format_instructions():获取格式说明

第二部分:提示词模板

| prompt

把上面的变量填进提示词模板里,生成完整的提示词。

第三部分:大模型

| llm

把提示词发给大模型,得到输出。

第四部分:输出解析

| self.fixing_parser

把大模型的输出解析成Action对象。如果解析失败,会自动让模型修正。

整个流程就是:输入 → 准备变量 → 生成提示词 → 调用大模型 → 解析输出 → 得到Action。

是不是挺清晰的?LCEL虽然一开始看着有点怪,但习惯了之后,写起来还是挺简洁的。


六、核心代码精讲:彻底搞懂每一行(下)

上半部分讲了Action.py和ReAct.py的前半部分,这一节继续讲剩下的核心代码。

6.1 ReAct.py 剩下的部分

6.1.1 单步执行
def step(self, query: str) -> Action:
        chain = self.build_chain()
        action = chain.invoke(query)
        return action

step方法就是执行一步,接收用户的问题,返回一个Action对象。

很简单,就是构建chain,然后invoke一下。

6.1.2 工具执行
def execute_tool(self, action: Action):
        # 检查工具是否存在
        if action.tool_name not in self.tool_names:
            return f"错误:不存在的工具 {action.tool_name},可用工具:{self.tool_names}"
        
        # 找到对应的工具
        tool = next(tool for tool in self.tools if tool.name == action.tool_name)
        
        # 执行工具
        try:
            result = tool.invoke(action.args)
            return result
        except Exception as e:
            return f"工具执行出错:{str(e)}"

这个方法是执行工具的,接收一个Action对象,返回执行结果。

逻辑也很简单:

  1. 先检查工具是否存在,不存在就返回错误信息

  2. 找到对应的工具

  3. 调用工具的invoke方法,传入参数

  4. 如果出错了,捕获异常,返回错误信息

这里的容错做得还是不错的,工具不存在、执行出错,都有处理。这样Agent就知道"哦,刚才那步出错了,我得想想下一步怎么办"。

6.1.3 单步Agent运行
def step_by_step(self, query: str) -> str:
        # 第一步:思考并选择工具
        action = self.step(query)
        print(f"思考:调用 {action.tool_name} 工具")
        print(f"参数:{action.args}")
        
        # 第二步:执行工具
        observation = self.execute_tool(action)
        print(f"观察:{observation}")
        
        # 第三步:生成最终回答(这里简化了,直接返回观察结果)
        # 完整的ReAct应该把观察结果再喂给模型,继续思考
        # 但这个项目目前是单步的,所以直接返回
        return observation

step_by_step方法是单步Agent的主入口。

目前这个项目的Agent是单步的——只调用一次工具就返回结果。完整的ReAct应该是多轮的,把观察结果再喂给模型,继续思考下一步。

为什么做成单步的?因为目前的场景比较简单,大部分问题调用一次RAG就够了。而且小模型多轮效果不一定好,容易跑偏。

不过代码结构是留好了的,以后想改成多轮的也很方便。

💡 怎么改成多轮?
其实很简单,就是加个循环:

  1. 思考 → 得到Action

  2. 执行工具 → 得到Observation

  3. 把Observation拼到提示词里,再思考

  4. 重复,直到模型觉得任务完成了

感兴趣的同学可以自己试试改一改,挺有意思的。

6.2 Tools.py:工具的注册和管理

讲完了Agent,我们来讲工具层。先看Tools.py,这个文件负责工具的注册和管理。

from langchain.tools.base import StructuredTool
from Tools.RagQATool import rag_qa
from Tools.ReportGenerationTool import generate_report

def get_tools():
    tools = [
        StructuredTool.from_function(
            func=rag_qa,
            name="rag_qa_tool",
            description="用于回答扫地机器人相关的问题,输入用户的问题,返回基于知识库的答案。当用户询问使用方法、故障排查、产品参数等问题时使用此工具。"
        ),
        StructuredTool.from_function(
            func=generate_report,
            name="report_generation_tool",
            description="用于生成各种分析报告,输入报告类型和时间范围,返回生成的报告内容。当用户要求生成报告、统计分析时使用此工具。"
        ),
    ]
    return tools

代码不长,我们来看看。

首先导入了StructuredTool,还有两个工具函数:rag_qa和generate_report。

然后get_tools函数返回一个工具列表。每个工具都是用StructuredTool.from_function创建的,把一个普通的Python函数包装成Agent能调用的工具。

每个工具需要三个东西:

  • func:工具对应的函数

  • name:工具的名字,Agent就是通过这个名字来调用的

  • description:工具的描述,告诉Agent这个工具是干啥的、什么时候用

💡 划重点
工具的description非常非常重要!它直接影响Agent能不能正确选择工具。
一个好的description应该说清楚:

  • 这个工具是干什么的

  • 什么时候该用这个工具

  • 输入输出是什么

  • 如果有适用范围或限制,也要说明

描述写得越清楚,Agent选得越准。

这个工具系统的设计我觉得挺不错的,添加新工具特别方便:

  1. 写一个新的函数

  2. 在get_tools里加一个StructuredTool

  3. 搞定!Agent那边完全不用改

这就是开闭原则——对扩展开放,对修改关闭。加新功能的时候尽量加新代码,不要改旧代码,这样不容易出bug。

6.3 RagQATool.py:RAG问答工具

接下来是RAG问答工具,这是项目里最核心的工具之一。

from langchain.chains import RetrievalQA
from Tools.VecStore import VecStore
from Models.Factory import llm

# 初始化向量库
vec_store = VecStore()
db = vec_store.get_db()

# 创建RAG链
rag_chain = RetrievalQA.from_chain_type(
    llm=llm,
    chain_type="stuff",
    retriever=db.as_retriever(search_kwargs={"k": 3}),
    return_source_documents=True,
)

def rag_qa(query: str) -> str:
    """
    RAG问答工具
    :param query: 用户问题
    :return: 回答
    """
    result = rag_chain.invoke(query)
    return result['result']

我们逐段来看。

初始化向量库:

vec_store = VecStore()
db = vec_store.get_db()

创建一个VecStore实例,然后获取向量数据库。这个VecStore我们后面会讲。

创建RAG链:

rag_chain = RetrievalQA.from_chain_type(
    llm=llm,
    chain_type="stuff",
    retriever=db.as_retriever(search_kwargs={"k": 3}),
    return_source_documents=True,
)

用LangChain的RetrievalQA来创建RAG链。一行代码就搞定了,是不是很方便?

参数说明:

  • llm:用哪个大模型

  • chain_type:链的类型,"stuff"是最简单的一种,就是把检索到的所有文档都塞进提示词里。还有其他类型,比如map_reduce、refine,适合文档比较多的情况

  • retriever:检索器,这里是从向量数据库创建的

  • search_kwargs={"k": 3}:检索的时候返回最相关的3个文档

  • return_source_documents=True:返回结果的时候,同时返回检索到的源文档。这样我们就能看到答案是从哪些文档来的,方便排查问题

问答函数:

def rag_qa(query: str) -> str:
    result = rag_chain.invoke(query)
    return result['result']

就是调用rag_chain的invoke方法,然后返回结果。

result是一个字典,里面有:

  • result:生成的答案

  • source_documents:检索到的源文档列表

💡 小技巧
调试的时候,可以把source_documents也打印出来,看看检索到的内容是不是相关的。如果检索的内容都不相关,那答案肯定好不了。这时候就要优化检索了。

6.4 VecStore.py:向量数据库管理

RAG的核心是向量数据库,我们来看看VecStore.py是怎么实现的。

这个文件稍微长一点,我分段讲。

6.4.1 导入和初始化
from langchain_community.vectorstores import Chroma
from langchain_community.document_loaders import PyPDFLoader, TextLoader, CSVLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from Models.Factory import embed_model
import os

CHROMA_PATH = "./Chroma"
DATA_PATH = "./data"

导入了一堆东西:

  • Chroma:Chroma向量数据库,轻量级,适合小项目

  • PyPDFLoaderTextLoaderCSVLoader:各种文档加载器

  • RecursiveCharacterTextSplitter:递归字符文本分块器

  • embed_model:向量模型

  • os:操作系统相关的函数

然后定义了两个路径:

  • CHROMA_PATH:向量数据库的存储路径

  • DATA_PATH:原始文档的路径

6.4.2 VecStore类
class VecStore:
    def __init__(self):
        self.db = None
        self._load_or_create_db()

初始化的时候调用_load_or_create_db方法,加载或者创建向量数据库。

6.4.3 加载文档
def _load_documents(self):
        documents = []
        
        for filename in os.listdir(DATA_PATH):
            filepath = os.path.join(DATA_PATH, filename)
            
            if filename.endswith('.pdf'):
                loader = PyPDFLoader(filepath)
                documents.extend(loader.load())
            elif filename.endswith('.txt'):
                loader = TextLoader(filepath, encoding='utf-8')
                documents.extend(loader.load())
            elif filename.endswith('.csv'):
                loader = CSVLoader(filepath)
                documents.extend(loader.load())
        
        return documents

这个方法是从data文件夹加载所有文档的。

逻辑很简单:

  1. 遍历data文件夹里的所有文件

  2. 根据文件后缀名,用对应的Loader来加载

  3. 把加载到的文档都加到列表里

  4. 返回文档列表

支持PDF、TXT、CSV三种格式。想支持更多格式的话,加对应的Loader就行。

6.4.4 文本分块
def _split_documents(self, documents):
        text_splitter = RecursiveCharacterTextSplitter(
            chunk_size=300,
            chunk_overlap=50,
            length_function=len,
        )
        splits = text_splitter.split_documents(documents)
        return splits

这个方法是把长文档切成小块的。

用的是RecursiveCharacterTextSplitter,递归字符分块器。它会尽量按照段落、句子、单词的顺序来切,尽量保持语义完整。

参数说明:

  • chunk_size=300:每块的大小,300个字符

  • chunk_overlap=50:块之间的重叠,50个字符。重叠是为了避免切分的时候把完整的意思切断

  • length_function=len:计算长度的函数,这里就是普通的len

💡 chunk_size怎么选?
这是一个经验值,没有标准答案。

  • 太大:每块信息太多,噪音多,还可能超上下文

  • 太小:上下文不完整,理解不了完整的意思

300是一个比较中庸的值,适合FAQ场景。你可以根据自己的情况调整,试试200、500,看看哪个效果好。

6.4.5 创建向量库
def _create_db(self):
        print("正在创建向量数据库...")
        
        # 加载文档
        documents = self._load_documents()
        print(f"加载了 {len(documents)} 个文档")
        
        # 分块
        splits = self._split_documents(documents)
        print(f"分成了 {len(splits)} 个块")
        
        # 创建向量库
        self.db = Chroma.from_documents(
            documents=splits,
            embedding=embed_model,
            persist_directory=CHROMA_PATH,
        )
        
        # 持久化
        self.db.persist()
        print("向量数据库创建完成")

创建向量数据库的流程:

  1. 加载文档

  2. 文本分块

  3. 用Chroma.from_documents创建向量库,传入文档、向量模型、存储路径

  4. 调用persist()保存到磁盘

Chroma.from_documents会自动把文档转向量,并存到数据库里。

6.4.6 加载向量库
def _load_db(self):
        print("正在加载向量数据库...")
        self.db = Chroma(
            persist_directory=CHROMA_PATH,
            embedding_function=embed_model,
        )
        print("向量数据库加载完成")

如果向量库已经存在了,就直接从磁盘加载,不用重新建。这样第二次启动就快多了。

6.4.7 判断是否需要重建
def _load_or_create_db(self):
        if os.path.exists(CHROMA_PATH) and os.listdir(CHROMA_PATH):
            self._load_db()
        else:
            self._create_db()

这个方法是判断该加载还是该创建:

  • 如果Chroma文件夹存在且不为空,就加载

  • 否则就创建

很简单的逻辑。

6.4.8 获取数据库
def get_db(self):
        return self.db

就是返回db对象,给外面用。

6.4.9 重新构建
def rebuild(self):
        # 删掉旧的
        import shutil
        if os.path.exists(CHROMA_PATH):
            shutil.rmtree(CHROMA_PATH)
        
        # 重新创建
        self._create_db()

重新构建向量库。就是把旧的删掉,重新建一个。

什么时候需要重建?比如知识库更新了,或者你改了分块参数,想重新试试效果。

💡 关于VecStore的设计
这个VecStore类封装得还是不错的,把向量库的创建、加载、重建都封装起来了。
以后如果想换向量数据库(比如换成FAISS),只要改这个类就行,其他地方不用动。
这就是封装的好处——把变化隔离在一个地方。

6.5 Factory.py:模型工厂

最后我们来看Models/Factory.py,模型工厂。

import yaml
from langchain_community.chat_models import ChatOllama
from langchain_community.embeddings import OllamaEmbeddings

# 加载配置
with open('./Configs/config.yml', 'r', encoding='utf-8') as f:
    config = yaml.safe_load(f)

class ChatModelFactory:
    """聊天模型工厂"""
    @staticmethod
    def create_chat_model():
        llm = ChatOllama(
            model=config['model_name'],
            base_url=config['server_url'],
            temperature=0.1,
        )
        return llm

class EmbeddingModelFactory:
    """向量模型工厂"""
    @staticmethod
    def create_embedding_model():
        embed_model = OllamaEmbeddings(
            model=config['embedding_name'],
            base_url=config['server_url'],
        )
        return embed_model

# 全局单例
llm = ChatModelFactory.create_chat_model()
embed_model = EmbeddingModelFactory.create_embedding_model()

代码不多,但设计得挺巧妙的。

工厂模式:
用了两个工厂类,ChatModelFactory和EmbeddingModelFactory,分别用来创建聊天模型和向量模型。

为什么要用工厂模式?

  1. 封装创建逻辑:创建模型的参数和配置都封装在工厂里,调用者不用关心

  2. 方便切换实现:以后想换成OpenAI的模型,只改工厂类就行,其他地方都不用改。因为大家依赖的是BaseChatModel这个抽象接口,不是具体的ChatOllama

  3. 统一配置管理:所有模型相关的配置都从config.yml读,不会散落在各处

单例模式:
最后在模块级别创建了两个全局单例:llm和embed_model。整个项目共用这两个实例。

好处:

  • 节省资源,不用反复创建

  • 方便使用,其他模块直接import就行

  • 统一配置,所有地方用的都是同一个模型

💡 小问题
这里配置文件的路径是相对路径’./Configs/config.yml’,这意味着你必须在项目根目录下运行程序,否则会找不到配置文件。
更健壮的写法是基于当前文件的位置来计算路径,这样不管从哪里运行都能找到。不过对于小项目来说,这样写也没问题。


七、运行效果展示

讲了这么多代码,咱们来看看实际跑起来是什么样的。

我就用Gradio界面来演示吧,有图有真相。

7.1 启动界面

启动之后,界面大概是这样的:

  • 左边是输入框,用户输入问题

  • 右边是输出框,显示回答

  • 可能还有一些参数设置,比如温度、检索数量啥的

(这里你可以放一张自己项目的截图,效果更好)

7.2 示例问答

示例1:问边刷怎么换

用户输入:“扫地机器人边刷怎么更换?”

系统大概会这样回答:

更换边刷的步骤如下:

1. 将扫地机器人翻转过来,底部朝上
2. 找到边刷的固定卡扣,轻轻按下卡扣
3. 将旧的边刷取下
4. 取出新的边刷,对准卡槽安装
5. 听到"咔哒"一声,说明安装到位
6. 将机器人翻转回来即可

注意:更换边刷时请确保机器人处于关机状态,避免意外启动。

你看,回答得还是挺像回事的,步骤清晰,还有注意事项。

示例2:问故障排查

用户输入:“扫地机器人卡住了怎么办?”

系统回答:

如果扫地机器人卡住了,可以按照以下步骤排查:

1. 首先按下电源键关机,避免强行拉扯造成损坏
2. 将机器人翻转过来,检查主刷和边刷是否被毛发、电线等缠绕
3. 如果有缠绕物,用清理工具小心清理
4. 检查轮子是否被异物卡住
5. 清理完成后,重新开机测试
6. 如果还是不行,可能是传感器故障,建议联系售后

注意:清理时不要用水直接冲洗机器人内部,以免造成电路损坏。

是不是还挺专业的?而且这些答案都是从知识库来的,不是模型瞎编的。

7.3 Agent的思考过程

如果打开调试模式,你还能看到Agent的思考过程:

用户问题:扫地机器人卡住了怎么办?

思考:用户问的是故障排查类问题,应该调用RAG问答工具来回答。
调用工具:rag_qa_tool
参数:{'query': '扫地机器人卡住了怎么办'}
观察:(上面那段回答)

最终回答:(返回给用户)

你看,Agent是有"思考"过程的——它先分析用户的问题是什么类型,然后决定调用哪个工具,然后执行工具,最后返回结果。

虽然现在是单步的,但已经能看出Agent的雏形了。

7.4 效果总结

总的来说,效果还是不错的:

  • ✅ 常见问题基本都能答对

  • ✅ 答案有依据,很少有幻觉

  • ✅ 回答流畅,像人写的

  • ⚠️ 速度一般,毕竟是本地小模型

  • ⚠️ 复杂问题处理得还不够好

当然,这只是基础版,还有很大的优化空间。后面我会讲怎么优化。


八、踩坑记录:那些让我头疼的问题

这部分是我最想分享的——踩坑记录。

做这个项目的过程中,我遇到了各种各样的问题,有的卡了我好几天。我把它们整理出来,希望你们能少走点弯路。

8.1 环境相关的坑

坑1:Ollama连不上

症状:运行的时候报错,说连不上Ollama。

原因:Ollama服务没启动,或者端口不对。

解决方法

  • 检查Ollama有没有在运行(Windows看任务栏,Mac/Linux看进程)

  • 检查config.yml里的server_url是不是对的

  • 浏览器打开http://localhost:11434,看能不能访问

坑2:模型拉不下来

症状:ollama pull的时候特别慢,或者直接失败。

原因:网络问题。

解决方法

  • 换个网络试试

  • 配置国内镜像源

  • 找别人拷一下模型文件(Ollama的模型文件是可以直接拷贝的)

坑3:虚拟环境搞混了

症状:明明装了依赖,运行的时候还是说找不到模块。

原因:没激活虚拟环境,或者装到别的环境里了。

解决方法

  • 运行前确认命令行前面有(.venv)

  • 用which python(Linux/Mac)或者where python(Windows)看看用的是哪个python

8.2 RAG相关的坑

坑4:检索不到相关内容

症状:问的问题知识库明明有,但回答说不知道,或者答非所问。

原因:可能的原因很多,我当时排查了好久。

排查步骤

  1. 先确认文档有没有正确加载——打印一下文档数量

  2. 再确认分块合不合理——打印几个块看看内容完不完整

  3. 再确认检索的k值是不是太小——调大一点试试

  4. 最后考虑是不是Embedding模型的问题——换个模型试试

我当时的问题是分块太大了,一个块里内容太多,检索不精准。后来把chunk_size调小了,效果就好多了。

坑5:答案和问题不相关

症状:检索到的内容是相关的,但生成的答案不对。

原因:提示词写得不好,或者模型太弱。

解决方法

  • 优化提示词,反复强调"请基于参考资料回答,不要编造"

  • 换更好的模型

  • 可以试试在提示词里加Few-Shot示例

坑6:中文乱码

症状:输出的中文是乱码。

原因:编码问题。

解决方法

  • 确保所有文件都用UTF-8保存

  • 读取文件的时候指定encoding=‘utf-8’

  • Windows下命令行可能需要改代码页:chcp 65001

8.3 Agent相关的坑

坑7:Agent不调用工具,直接回答

症状:问的问题应该要调用RAG,但Agent直接凭记忆回答了,而且答案不对。

原因:提示词不够强,或者模型太小,不听话。

解决方法

  • 优化提示词,反复强调"必须调用工具,不要直接回答"

  • 加Few-Shot示例,给它看几个正确调用工具的例子

  • 换更大一点的模型,小模型确实更容易不听话

我当时被这个问题卡了好久,后来加了OutputFixingParser,又优化了提示词,情况才好转。

坑8:Agent调用不存在的工具

症状:Agent输出的tool_name不在工具列表里。

原因:模型幻觉,或者工具描述写得不好。

解决方法

  • 优化工具描述,写得更清楚

  • 在提示词里反复强调只能用哪些工具

  • 加异常处理,调用失败了返回错误信息,让Agent重试

坑9:输出格式不对,解析失败

症状:JSON解析错误,或者Pydantic验证失败。

原因:模型输出的格式不规范。小模型经常这样。

解决方法

  • 一定要用OutputFixingParser!一定要用!一定要用!(重要的事说三遍)

  • 优化提示词,强调输出格式

  • 加Few-Shot示例,给它看正确的格式

  • 可以再加一层兜底,比如用正则表达式提取JSON

坑10:Agent跑偏了

症状:多轮对话的时候,Agent越跑越偏,最后不知道在干啥。

原因:小模型的多轮能力不行。

解决方法

  • 如果场景简单,就用单步的(像我们这个项目一样)

  • 一定要做多轮的话,换更大的模型

  • 优化提示词,每轮都强调任务目标

8.4 工程相关的坑

坑11:路径问题

症状:找不到文件,比如"FileNotFoundError: Configs/config.yml"。

原因:运行目录不对,相对路径是相对于当前工作目录的。

解决方法

  • 确保在项目根目录下运行脚本

  • 或者把相对路径改成基于文件位置的绝对路径

坑12:依赖版本冲突

症状:安装依赖的时候报错,或者运行的时候说某个函数不存在。

原因:库的版本不对,LangChain的不同版本API变化还挺大的。

解决方法

  • 尽量用requirements.txt里指定的版本

  • 如果要升级,注意看官方的变更日志

  • 出问题了先查一下是不是版本问题


九、优化思路:这个项目还能怎么改进?

基础版跑通了只是开始,后面能优化的地方还多着呢。我整理了一些优化方向,你可以试试。

9.1 RAG优化

RAG的优化空间是最大的,也是最容易出效果的。

1. 优化分块策略

  • 试试按语义分块,而不是固定大小

  • 试试父子块(小块检索,大块返回)

  • 不同类型的文档用不同的分块策略

2. 优化检索方式

  • 混合检索:关键词检索 + 向量检索,结合两者的优势

  • 加重排序:先粗排召回一批,再用更好的模型精排

  • 多查询检索:把用户的问题改写成几个问法,分别检索再合并

3. 优化生成

  • 优化提示词,这个是成本最低但效果最明显的

  • 试试不同的chain_type,比如refine、map_reduce

  • 加入引用标注,让答案标明哪些内容来自哪里

4. 优化Embedding模型

  • 换更好的Embedding模型,检索效果会提升

  • 针对垂直领域微调Embedding模型(如果有数据的话)

9.2 Agent优化

1. 从单步改成多轮

  • 实现完整的ReAct循环

  • 加入终止条件,让模型自己判断什么时候结束

2. 优化提示词

  • 加Few-Shot示例

  • 加入思考引导,让模型一步步想

  • 加入错误处理的说明

3. 增加更多工具

  • 计算器工具

  • 搜索引擎工具

  • 数据库查询工具

  • 代码执行工具

  • ...

工具越多,Agent的能力就越强。

4. 加入记忆

  • 支持多轮对话,记住之前的对话内容

  • 短期记忆:当前会话的上下文

  • 长期记忆:用户的偏好、历史问题等

9.3 工程优化

1. 增加评估体系

  • 做一个评估集,准备一批问题和标准答案

  • 量化评估准确率、相关性、完整性等指标

  • 每次优化后跑一遍评估,看效果有没有提升

2. 增加日志系统

  • 记录每一次请求的详细信息

  • 方便排查问题和分析效果

  • 可以用来做bad case分析

3. 性能优化

  • 缓存常用问题的答案

  • 异步处理,提高并发

  • 模型量化,加速推理

4. 部署优化

  • 用Docker打包,方便部署

  • 加入监控和告警

  • 做负载均衡


十、我的收获和体会

最后,聊聊我做这个项目的收获和体会吧。

10.1 技术上的收获

1. 搞懂了RAG和ReAct
以前只是听说过这些概念,现在自己动手实现了一遍,理解就深刻多了。很多东西,光看是看不懂的,得自己做一遍才明白。

2. 学会了用LangChain
这个框架确实挺强大的,很多功能都封装好了,能大大提高开发效率。当然,封装得深也有缺点,出问题不好排查,但总体来说还是利大于弊。

3. 学会了本地部署大模型
以前总觉得大模型很高大上,得有服务器才能跑。现在发现,普通电脑也能跑小模型,虽然效果差点,但用来学习和做小项目完全够用。

4. 对大模型应用开发有了整体认识
原来大模型应用开发不是调个API就完事了,里面有很多工程化的东西——提示词工程、检索优化、容错处理、评估等等,学问大着呢。

10.2 工程上的体会

1. 分层和模块化真的很重要
这个项目的分层架构我觉得做得不错,每层职责清晰,改起来也方便。以前写代码总喜欢堆在一起,现在才体会到模块化的好处。

2. 容错设计不能少
大模型应用和传统应用不一样,传统应用的输入输出都是确定的,而大模型的输出是不确定的,你永远不知道它会输出什么奇奇怪怪的东西。所以容错设计特别重要,比如OutputFixingParser、异常捕获这些,都得有。

3. 调试和排错能力很重要
做这个项目,我一半的时间都在排错。环境问题、依赖问题、模型输出不对、检索不准...各种各样的问题。但排错排多了,能力也就上来了。现在遇到问题,我大概知道该从哪下手排查了。

10.3 认知上的改变

1. 大模型不是万能的
以前觉得大模型无所不能,现在发现不是。大模型有它擅长的地方,也有它不擅长的地方。得和传统技术结合起来,才能发挥最大的价值。

2. Agent是个很有前景的方向
让大模型从"会聊天"变成"会做事",这个想象空间太大了。以后可能会有各种各样的Agent,帮我们处理各种工作。

3. 工程能力同样重要
AI项目不是只有算法,工程能力也很重要。怎么把模型落地成一个好用、稳定、高效的系统,这里面的学问一点也不比算法少。


十一、总结

好了,啰啰嗦嗦写了这么多,差不多该收尾了。

这篇文章从项目背景、核心概念、环境搭建、项目结构、核心代码、运行效果、踩坑记录,一直讲到优化思路和个人体会,算是把这个项目从头到尾梳理了一遍。

如果你能耐心看到这里,相信你对ReAct+RAG的智能问答Agent已经有了比较全面的了解。但我还是想说,光看是不够的,一定要自己动手做一遍。编程这东西,纸上得来终觉浅,绝知此事要躬行。

这个项目虽然不算复杂,但五脏俱全,涵盖了大模型应用开发的很多核心知识点。把它搞懂了,再去做更复杂的项目,就会容易很多。

如果你在做的过程中遇到什么问题,欢迎在评论区留言,我们一起讨论。

最后,如果这篇文章对你有帮助,别忘了点赞、收藏、关注三连~ 你们的支持是我继续分享的动力!

我们下篇文章见。


好了,博客内容就到这里。你可以把这些内容复制保存成Markdown文件,或者直接发布到CSDN上。整篇文章大概1.5万字左右,语言都是口语化的,有干货也有踩坑记录,挺符合CSDN的风格的。

如果觉得哪里需要调整,比如想加内容、改风格、调结构,随时告诉我。

(注:部分内容可能由 AI 生成)

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐