Agent智能体学习总结:从一次调用到模型自主规划、工具调用与 ReAct 循环的项目实战-6/6

**摘要:**本文系统梳理了 Agent 智能体的核心概念与实战落地路径。从"调模型"到"模型自主规划+调用工具"的认知升级出发,依次拆解了 Agent 的三大核心要素(规划、工具、记忆)、最小 Agent 的搭建与流式输出、ReAct(思考→行动→观察)循环范式,以及 middleware 中间件的六大执行时机。后半部分以"智家通智能客服"项目为主线,完整呈现了日志、路径、配置、提示词加载、向量存储、RAG 总结、业务工具与中间件的工程化组装过程,帮助读者从原理到代码,真正理解并跑通一个 RAG+Agent 的综合系统。

目录

  • 一、模块定位与学习目标:从"调模型"升级到"模型自主规划+调用工具"
  • 二、知识块拆解
    • [2.1 Agent 是什么:大脑+手脚+思维,三部分合在一起](#2.1 Agent 是什么:大脑+手脚+思维,三部分合在一起)
    • [2.2 初体验:跑通一个最小 Agent](#2.2 初体验:跑通一个最小 Agent)
    • [2.3 Agent 的流式输出:一条消息一条消息地蹦](#2.3 Agent 的流式输出:一条消息一条消息地蹦)
    • [2.4 ReAct 行动框架:Thought → Action → Observation 循环](#2.4 ReAct 行动框架:Thought → Action → Observation 循环)
    • [2.5 middleware 中间件:在执行的关键节点插钩子](#2.5 middleware 中间件:在执行的关键节点插钩子)
  • [三、项目实战:智家通智能客服(第 06--13 集)](#三、项目实战:智家通智能客服(第 06–13 集))
    • [3.1 项目介绍:一个 RAG+Agent 的综合系统](#3.1 项目介绍:一个 RAG+Agent 的综合系统)
    • [3.2 日志与路径工具(第 07 集)](#3.2 日志与路径工具(第 07 集))
    • [3.3 配置、文件与提示词加载工具(第 08 集)](#3.3 配置、文件与提示词加载工具(第 08 集))
    • [3.4 向量存储服务(第 09 集)](#3.4 向量存储服务(第 09 集))
    • [3.5 RAG 总结服务(第 10 集)](#3.5 RAG 总结服务(第 10 集))
    • [3.6 tools 工具开发(第 11 集)](#3.6 tools 工具开发(第 11 集))

李沐深度学习191集课程全解析:模块拆解、学习路径-CSDN博客

吴恩达《面向开发者的提示词工程》-CSDN博客

吴恩达 MCP 教程(Model Context Protocol)(一)-CSDN博客

多模态大模型教程学习笔记 --- ViT · CLIP · SAM · GLIP · Stable Diffusion

一句话总结: Agent 智能体模块的核心,是把大模型从"只会回答问题"升级成"会自己想步骤、自己挑工具、自己根据工具结果决定下一步"的执行者------在 LangChain 里用 create_agent 把"模型(大脑)+工具集(手脚)+中间件(拦截钩子)"组装起来,并以 ReAct(思考→行动→观察)为运行范式,最终落地成一个能问答、能自动生成使用报告的扫地机器人智能客服项目。

本模块对应黑马程序员《大模型 RAG 与 Agent 智能体项目实战教程》中"Agent 智能体"部分的 13 集(编号 24--36)。技术栈围绕新版 LangChain 的 agents.create_agent、@tool 工具装饰器、stream_mode="values" 流式、middleware 中间件,以及底层依赖的 LangGraph 运行时(AgentState、runtime、Command)展开。下面按"概念---范式---项目---踩坑"的顺序,把这 13 集写透。

一、模块定位与学习目标:从"调模型"升级到"模型自主规划+调用工具"

在进入 Agent 之前,前面的 RAG 模块我们已经学会了:把提示词模板、模型、输出解析器用 | 串成一条固定的执行链,或者用 RAG 把知识库检索结果塞进提示词再让模型回答。但这些都有一个共同特点------流程是我们在代码里写死的:第一步干什么、第二步干什么、调不调外部接口,人已经决定好了,模型只负责在每一步"填空"。

Agent 模块要解决的是另一种问题:用户抛来一句开放的话("帮我看看我所在城市的天气下,扫地机器人该怎么保养"、"帮我生成最近的使用报告"),我们事先根本不知道该调几个工具、按什么顺序调。这时候不能再手写固定链路,而要把"要不要调、调哪个、调完够不够"这个决策权交给模型自己。

所以本模块的学习目标可以概括为三点:

  1. 理解 Agent 与普通链式调用、与纯 RAG 的本质区别;

  2. 掌握用 create_agent 组装一个最小可用 Agent,并能读懂它的流式输出;

  3. 通过一个完整项目(智家通智能客服),把日志、路径、配置、文件、提示词加载、向量检索、RAG 总结、业务工具、中间件、Agent 创建、Streamlit 界面这一整套工程化骨架搭出来。

二、知识块拆解

2.1 Agent 是什么:大脑+手脚+思维,三部分合在一起

课程对智能体下的定义很直白:智能体(Agent)是一种能够自主规划、决策并执行任务的组件,它的核心是让大语言模型根据用户的任务需求,自己选择并调用合适的工具,去完成单靠模型自身训练数据无法解决的复杂问题。

没有 Agent 时,大模型只能靠训练数据回答问题,遇到实时数据、复杂计算、外部系统调用就会卡壳(比如问"明天深圳天气",通义千问会很礼貌地回你"我无法提供实时天气,请打开手机 App 查询")。有了 Agent 之后,模型像一个指挥官:先思考该做什么,再选工具、执行工具,然后根据工具返回的结果决定是否继续。

课程给出的 Agent 三大核心要素是:规划、工具、记忆。

  • 规划(思维):用户需求进来后,模型不直接回答,而是先想"要完成这个需求我得做哪几件事";

  • 工具(手脚):大模型本身没有行动力,工具就是给它外挂的"手和脚",让它能查天气、查数据库、读文件、检索知识库;

  • 记忆:整个执行过程中会保存短期记忆(消息列表),模型根据之前已经发生了什么(调过什么工具、拿到了什么结果)动态调整下一步。

由此引申出三个特点:目标驱动(围绕用户具体任务展开)、工具调用能力(连接外部工具弥补模型局限)、自主决策与迭代(不需要人工干预,根据工具结果判断要不要再调一个工具)。

课程用一个电商例子串了一遍:用户问"我们店铺销量最高的三款手机,最近七天的退货率分别是多少?"------Agent 思考后判断"我需要查询销售数据",于是调用数据库查询工具并传入参数,拿到结果后整理成自然语言回答;如果数据还不够,它会再思考"是不是还要取别的数据",再调一次工具,经过多轮迭代才产出最终答案。

一句话公式:Agent = 大语言模型(大脑)+ 工具集(手脚)+ 决策逻辑(思维)。英文里 Agent 直译是"代理",意思就是大模型只负责"思考该用什么工具",而 Agent 这个"代理"负责真正去调用工具、把结果交还给模型再思考。正是这套组合,让大模型从"只会回答文字"升级为"能真正做事、影响现实世界"(发邮件、写报告、回消息、查数据)。

普通调用 / RAG / Agent 三者对比
维度 直接调用模型 RAG 检索增强 Agent 智能体
执行流程 固定,一次进出 半固定:先检索再拼提示词再回答 动态,由模型边想边决定
谁决定下一步 代码写死 代码写死检索+回答两步 模型自己决定调哪个工具、调几次
能否调外部工具 不能 只能调向量库检索这一件事 任意多个工具,可循环调用
记忆 无 无(或临时会话记忆) 带短期消息记忆,据此迭代
适合任务 简单问答、改写 有知识库、要事实准确的问答 复杂、多步骤、需要决策的任务
典型产物 一段文字 带引用的回答 多轮思考后产出的答案或报告

2.2 初体验:跑通一个最小 Agent

概念讲完,课程立刻上手写了第一份代码(01_agent智能体初体验)。关键 API 是从 langchain.agents 里导入的 create_agent (注意不是旧版的 initialize_agent / AgentExecutor,这是新版 LangChain 的写法)。它的三个核心参数:

  • model:智能体的"大脑",课程用的是 ChatTongyi(通义千问,model="qwen-max");

  • tools:工具列表,可以传多个;

  • system_prompt:系统提示词,比如"你是一个聊天助手,可以回答用户问题"。

工具的本质就是一个普通 Python 函数,只要在上面加一个 @tool 装饰器 (来自 langchain_core.tools)并写上 description,它就变成了 Agent 可调用的工具。课程写了一个 get_weather:

python 复制代码
from langchain.agents import create_agent
from langchain_core.tools import tool
from langchain_community.chat_models.tongyi import ChatTongyi
model = ChatTongyi(model="qwen-max")
@tool
def get_weather() -> str:
"""查询天气"""
return "晴天"
agent = create_agent(
model=model,
tools=[get_weather],
system_prompt="你是一个聊天助手,可以回答用户问题",
)

Agent 对象本身是 Runnable 接口的子类,所以和链一样有 invoke / stream。但它的入参格式不一样:要传一个字典,key 必须是 "messages" (带 s),value 是消息列表:

python 复制代码
result = agent.invoke({
    "messages": [
        {"role": "user", "content": "明天深圳的天气如何?"},
    ]
})

返回值同样是 {"messages": [...]} 形式的消息列表。课程用 for msg in result["messages"] 逐条打印,并用 type(msg).__name__ 来看每条消息是谁说的,于是观察到一条非常典型的执行轨迹:

  1. HumanMessage:用户提问"明天深圳天气如何";

  2. AIMessage:content 是空的------这说明模型这一轮没有直接回答,它只是做了思考与决策,决定要调用工具;

  3. ToolMessage:工具返回"晴天",相当于工具把结果喂回给模型;

  4. AIMessage:模型拿到"晴天"这个事实后,才组织出最终答案"明天深圳天气是晴天,适合户外活动"。

这个空 content 的 AIMessage 是理解 Agent 的关键:模型不是一次性把答案吐出来,而是先输出"我要调工具"这个决策,由框架执行工具后,再带着工具结果进入下一轮 。没有工具时,模型只能回答"我查不了天气";加上 get_weather 后,模型自己判断"这个问题我得查工具",于是整个能力边界被扩展了。

课程还强调:工具越多,Agent 能覆盖的业务场景越广(客服问答、库存管理、自动化运营)。搞智能体,本质就是"提供工具集+借大模型的决策来决定这些工具怎么被调用"。

2.3 Agent 的流式输出:一条消息一条消息地蹦

第二个文件(02_agent的stream流式输出)解决体验问题。agent.stream(...) 用法和 invoke 入参一样,都是 {"messages": [...]},但通常要再传第二个参数 stream_mode="values":

python 复制代码
for chunk in agent.stream(
    {
        "messages": [
            {"role": "user", "content": "传智教育股价多少?并介绍一下"},
        ],
    },
    stream_mode="values",
):
    ...
python 复制代码
for chunk in agent.stream(
    {"messages": [{"role": "user", "content": "传智教育股价多少?并介绍一下"}],},
    stream_mode="values",
):
    ...

Agent 流式和 RAG 流式有个重要区别:RAG 是一个字一个字地蹦,而 Agent 是一条消息一条消息地蹦 。而且每个 chunk 都是一个完整字典 {"messages": [......整条历史......]},也就是说每一片都带着到目前为止的全部历史消息 ,越往后 chunk 越长。所以正确的取法不是从头解析,而是只取最后一条:

python 复制代码
latest = chunk["messages"][-1]
python 复制代码
latest = chunk["messages"][-1]

拿到最新消息后,分两种情况处理:

  • 如果 latest.content 有内容 ,说明是一段可以展示的文字(AI 回复或工具回包),打印它,并用 type(latest).__name__ 标注是哪种消息;

  • 如果 latest.tool_calls 有内容 ,说明这一轮模型决定调用工具。tool_calls 是一个列表(模型可能一次性并行调多个工具),每个元素是个字典,取 tc["name"] 就能拿到工具名。

这里有个真实的坑:不是每条消息都有 tool_calls 属性 。比如第一条 HumanMessage 根本没有这个属性,直接访问会抛 AttributeError。课程的做法是用 try/except AttributeError 包起来,没有就 pass:

python 复制代码
for chunk in agent.stream(input_dict, stream_mode="values"):
    latest = chunk["messages"][-1]
    if latest.content:
        print(type(latest).__name__, ":", latest.content)
    try:
        if latest.tool_calls:
            names = [tc["name"] for tc in latest.tool_calls]
            print("工具调用:", names)
    except AttributeError:
        pass
python 复制代码
for chunk in agent.stream(input_dict, stream_mode="values"):
    latest = chunk["messages"][-1]
    if latest.content:
        print(type(latest).__name__, ":", latest.content)
    try:
        if latest.tool_calls:
            names = [tc["name"] for tc in latest.tool_calls]
            print("工具调用:", names)
    except AttributeError:
        pass

课程这一节写了两个股票工具 get_price(name) 和 get_info(name),并特意在系统提示词里写"请告知我思考过程,让我知道你为什么调用某个工具"。跑"传智教育股价多少并介绍一下"时,能观察到模型一轮里同时并行调用了两个工具 (股价+公司介绍),然后 ToolMessage 把两个结果都带回来,最后 AIMessage 汇总成一段完整回答。这也印证了:LangChain 的 Agent 天然支持并行工具调用。

2.4 ReAct 行动框架:Thought → Action → Observation 循环

第 04 集讲的是整个智能体最核心的思考范式------ReAct = Reason(推理)+ Act(行动)。它让 Agent 不再拿到问题就直接回答,而是先用自然语言"想一遍",用思考去指导工具调用,一步步把复杂问题拆解掉。

课程画的循环是这样的:用户输入(input)进来后,模型先思考(Thought) ------注意这里不是生成答案,而是想"我该怎么做、缺什么信息";思考落实成行动(Action) ,也就是调用某个工具;工具返回结果后,模型进入观察(Observation) ,看"这个结果对不对、够不够";如果不够,就再思考下一步、再行动、再观察;直到信息足够,才 Finish 输出最终答案。

课程用一个 BMI 计算器案例把这个循环演了一遍。用户问"计算我的 BMI",但 BMI 需要体重和身高,模型并不知道。于是准备了两个工具:get_weight() 返回 90(kg)、get_height() 返回 172(cm)。系统提示词里明确约束模型"严格遵循 ReAct,按思考---行动---观察---再思考的流程解决问题"。

课程特别说明:为了让大家看清多轮迭代,演示时刻意加了"每轮只调一个工具、禁止并行调用"的限制------这其实不符合实际,因为 LangChain 是支持并行工具调用的,这么写纯粹是教学需要。

ReAct 循环步骤表(BMI 案例)
轮次 阶段 模型内部在做什么 对外动作
1 Thought 算 BMI 需要体重和身高,目前都缺 输出思考
1 Action 先取体重(演示要求一次只取一个) 调 get_weight
1 Observation 拿到体重 90kg,信息正确,但还缺身高 观察结果
2 Thought 有了体重,下一步该取身高了 输出思考
2 Action 调用取身高工具 调 get_height
2 Observation 拿到身高 172cm,信息正确 观察结果
3 Thought 体重、身高都齐了,可以套公式算了 输出思考
3 Finish 按 BMI 公式计算并给出结果 输出最终答案

这个例子要记住的三句话:思考=分析需求、考虑下一步;行动=用工具拿到所需信息;观察=分析拿到的信息对不对、够不够。 整个 Agent 就是"思考→行动→观察→思考→行动→观察"地转,直到结束。而我们用 create_agent 造出来的 Agent 对象,本质上就是按 ReAct 模式运行的,不需要我们手写循环。

2.5 middleware 中间件:在执行的关键节点插钩子

第 05 集讲中间件。它的作用是对智能体的每一步工作进行拦截和自定义 。典型场景有:日志记录、提示词改写、失败重试、安全防护、身份检测等。课程的比喻是:没有中间件时,模型按 ReAct 范式一路跑到底;有了中间件,从请求发起到拿到结果之间的六个时间节点都能插一脚。

课程把 LangChain 内置的中间件钩子分成两类共六种:

  • 节点式钩子(按执行顺序拦截) :before_agent、after_agent、before_model、after_model;

  • 包装式钩子(包住每一次模型/工具调用) :wrap_model(每次模型调用时拦截)、wrap_tool(每次工具调用时拦截)。

Middleware 六大执行时机表
装饰器 时机 函数入参形态 典型用途
@before_agent 整个 Agent 启动前 (state: AgentState, runtime) 打印启动日志、初始校验
@after_agent 整个 Agent 结束后 (state: AgentState, runtime) 收尾、统计、落库
@before_model 每次调用模型前 (state: AgentState, runtime) 打印即将发给模型的消息数/内容
@after_model 每次调用模型后 (state: AgentState, runtime) 看模型返回、消息数变化
@wrap_model 包住每一次模型调用 (request, handler) 记录模型请求/重试
@wrap_tool 包住每一次工具调用 (request, handler) 监控工具名、入参、成败

两类钩子的函数签名不一样,这是最容易混的地方:

python 复制代码
from langchain.agents.middleware import before_agent, after_agent, before_model, after_model
from langchain.agents.middleware import wrap_tool
from langchain.agents.middleware import wrap_model
python 复制代码
from langchain.agents.middleware import before_agent, after_agent, before_model, after_model
from langchain.agents.middleware import wrap_tool
from langchain.agents import AgentState
from langgraph.runtime import Runtime

@before_agent
def log_before_agent(state: AgentState, runtime: Runtime) -> None:
    print(f"[before_agent] agent 启动,附带 {len(state['messages'])} 条消息")

节点式钩子的入参是 state (AgentState,里面有 messages 消息列表)和 runtime ,一般返回 None,你在里面想干什么就干什么。

包装式钩子的入参则是 request (这次调用的请求封装)和 handler (真正干活的那个函数) 。你想"原封不动放行",就 return handler(request),但可以在前后插日志:

python 复制代码
@wrap_tool
def monitor_tool(request, handler):
    # request.tool_call["name"] 取工具名,request.tool_call["args"] 取入参
    print(f"[tool] 执行工具 {request.tool_call['name']},参数:{request.tool_call['args']}")
    return handler(request)
python 复制代码
@wrap_tool
def monitor_tool(request, handler):
    # request.tool_call["name"] 取工具名,request.tool_call["args"] 取入参
    print(f"[tool] 执行工具 {request.tool_call['name']},参数:{request.tool_call['args']}")
    return handler(request)

最后把这些钩子函数像工具一样装进 create_agent 的 middleware=[...] 列表即可。课程演示"深圳今天天气如何、如何穿衣"时,把六个钩子全挂上,控制台依次打印出非常清晰的执行顺序:

  1. before_agent:agent 启动,附带 1 条消息(用户提问);

  2. before_model:模型即将调用,附带 1 条消息;

  3. wrap_model:模型被调用;

  4. after_model:模型调用结束,附带 2 条消息(多出来的那条是"要调工具"的决策消息);

  5. wrap_tool:执行 get_weather,参数 city=深圳;

  6. 工具结果打包,模型又被调用一次;

  7. after_model:模型调用结束,附带 4 条消息;

  8. after_agent:整个 agent 结束,最终回答"今天是晴天......"。

这段日志把 ReAct 的内部状态完全外显了------这正是中间件最大的价值:让原本黑盒的 Agent 执行过程变得可观察、可干预。

三、项目实战:智家通智能客服(第 06--13 集)

3.1 项目介绍:一个 RAG+Agent 的综合系统

从第 06 集开始进入项目。项目名叫智家通,是一个面向消费者的智能客服系统,为扫地机器人(含洗拖一体)提供全周期服务。它有两大核心:

  1. 智能问答服务:基于前面学过的 RAG,从知识库检索信息后回答用户问题;

  2. 使用报告生成:这是本项目 Agent 的核心工作方向------根据已购买用户的历史使用记录,自动生成一份使用报告。

课程提供了 6 份知识库数据(故障排除、常见问答、维护保养等),全部灌入向量数据库。整个代码体量比之前的 RAG 项目多了三倍以上,结构上分成 RAG 部分和智能体部分两块。

这里有一个非常关键的架构思想 ,必须单独点出来:在这个项目里,RAG 不是最终目的,RAG 本身被包装成了 Agent 的一个普通工具 。流程是------从向量库检索到参考资料后不结束,而是把参考资料交给模型做一次"总结概括",产出的 rag_summary 函数注册成一个 @tool。Agent 需要查资料时才调用它,不需要就不调。换句话说,这个项目本质是面向智能体的,RAG 只是给智能体提供的一把"检索工具"。

除了 RAG 工具,项目还准备了:天气查询、获取用户定位、获取用户 id、获取当前月份、从外部系统抽取用户使用记录等工具;中间件则有工具监控、模型调用前日志,以及一个核心的"动态提示词切换"标志。最后把工具和中间件全部装进 Agent,再包一个 execute_stream 流式函数,结果送到 Streamlit 网页上。

Agent 项目模块划分表
模块文件 类/关键函数 职责
utils/log_handle.py log 统一日志(控制台+按日期的本地文件,分级 info/debug)
utils/path_tools.py get_abs_path 把相对路径转成绝对路径,避免跑错目录
utils/config_handle.py agent_cfg 读取配置(如外部数据文件路径)
utils/file_handle.py 文件读写工具 通用文件处理
utils/prompt_loader.py load_system_prompt / load_rag_prompt / load_report_prompt 加载三份提示词文本
model/factory.py chat_model 统一提供 ChatTongyi 模型实例
rag/vector_store.py VectorStoreService / get_retriever 向量存储与检索器
rag/rag_service.py RAGSummaryService 检索+让模型总结,作为 RAG 总结服务
agent/tools/agent_tools.py 6+1 个 @tool 注册给 Agent 的全部工具
agent/middleware.py monitor_tool / log_before_model / report_prompt_switch 三个自定义中间件
agent/react_agent.py ReActAgent / execute_stream 组装 Agent 并对外流式
app.py Streamlit 页面 用户交互入口

3.2 日志与路径工具(第 07 集)

这一集搭工程地基。log_handle 提供一个全局 log 对象,后续代码统一用 log.info / log.warning / log.error / log.debug 打日志。课程的设计是双输出:控制台只显示 info 及以上级别(干净),而本地按日期命名的日志文件里保留全部 debug 细节------这样线上排障时能看到每条消息的完整内容,日常开发控制台又不被刷屏。

path_tools.get_abs_path 解决的是"相对路径在不同启动目录下失效"的经典问题。凡是要读配置、读数据文件,都不直接写相对路径,而是先 get_abs_path(...) 转成绝对路径再用。后面读外部 CSV 时还配合 os.path.exists 做判断:

python 复制代码
if not os.path.exists(external_path):
    raise FileNotFoundError(f"外部数据文件不存在:{external_path}")

地基虽小,但后面所有工具、中间件都靠这两样------没有统一日志,Agent 的黑盒执行根本无从排查;没有绝对路径,换个目录运行就会报文件找不到。

3.3 配置、文件与提示词加载工具(第 08 集)

这一集继续补 utils:

  • 配置工具 config_handle.agent_cfg :把路径这类易变项从代码里抽出来放配置。后续读取外部数据时,就是 agent_cfg 里配好的 data/external/records.csv 路径,再经 get_abs_path 转绝对路径;

  • 文件工具 file_handle:封装通用的读写;

  • 提示词加载 prompt_loader :这是本项目的一大亮点------项目提示词非常长(主提示词、RAG 总结提示词、报告生成提示词三份),课程没有把它们硬编码进 Python,而是放在独立文本文件里,用 load_system_prompt()、load_rag_prompt()、load_report_prompt() 三个函数加载。

把提示词从代码里抽离的好处很实际:改提示词不用动 Python 逻辑、不用重启调试框架,调 prompt 就是改文本文件。这也是为什么后面"动态切换提示词"能轻松做到------只要按条件调不同的 loader 函数即可。

3.4 向量存储服务(第 09 集)

这一集把前面 RAG 模块学过的向量存储封装成 VectorStoreService。它对外暴露一个 get_retriever() 方法,返回一个检索器。这个设计的用意是复用前序 RAG 的向量库成果 :6 份知识库文档已经切块、向量化并入库,Agent 项目不重复造轮子,只需要拿到一个能 invoke(query) 返回 list[Document] 的检索器即可。

后面 RAG 总结服务、乃至 Agent 的 RAG 工具,都是站在这个向量存储服务之上的。可以把它理解成整个项目的"知识底座"。

3.5 RAG 总结服务(第 10 集)

rag_service.py 里的 RAGSummaryService 是 RAG 部分的核心。它的职责不是直接给用户最终答案,而是:用户提问 → 检索参考资料 → 把"提问+参考资料"一起交给模型 → 让模型产出一段简洁概括。这个类的私有成员基本是一个"流水线零件清单":

python 复制代码
from rag.vector_store import VectorStoreService
from utils.prompt_loader import load_rag_prompt
from model.factory import chat_model
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.documents import Document

class RAGSummaryService:
    def __init__(self):
        self.vector_store = VectorStoreService()
        self.retriever = self.vector_store.get_retriever()
        self.prompt_text = load_rag_prompt()
        self.prompt_template = PromptTemplate.from_template(self.prompt_text)
        self.model = chat_model
        self.chain = self._init_chain()

    def _init_chain(self):
        return self.prompt_template | self.model | StrOutputParser()

两个核心方法:

python 复制代码
def retrieve_docs(self, query: str) -> list[Document]:
    return self.retriever.invoke(query)

def rag_summary(self, query: str) -> str:
    docs = self.retrieve_docs(query)
    context = ""
    counter = 0
    for doc in docs:
        counter += 1
        context += f"参考资料{counter}: {doc.page_content} | 参考元数据: {doc.metadata}\n"
    return self.chain.invoke({"input": query, "context": context})

注意几个细节:

  1. 检索到的文档被拼成带序号的 context 字符串("参考资料1: ... | 参考元数据: ..."),序号让模型能区分多条来源;

  2. 提示词模板里定义了两个变量 input(用户问题)和 context(参考资料),invoke 时一次注入;

  3. RAG 总结提示词本身是一份很规范的提示词:要求模型"你是善于总结的助手",并给出内容合规、事实准确、语言要求、聚焦、格式要求等硬约束。

课程还复用了 RAG 模块的老调试套路:临时往链中间插一个 print_pm 函数,把最终发给模型的完整提示词 pm.to_string() 打出来,确认 input 和 context 都正确注入了。

最后再次强调定位:rag_summary() 这个方法,下一步会被包成一个 @tool ,成为 Agent 众多工具中的一个。

3.6 tools 工具开发(第 11 集)

这一集在 agent/tools/agent_tools.py 里,把 Agent 要用到的工具一个个 @tool 化。课程特别强调:@tool 的 description 是必填的,不写会报错------因为模型正是靠这段描述来判断"什么时候该用这个工具"。

项目工具清单表
工具函数 入参 返回 用途
rag_summary query: str 总结字符串 从向量库检索参考资料并总结
get_weather city: str 天气字符串 返回指定城市天气(演示用固定值)
get_user_location 无 城市名 随机返回用户所在城市
get_user_id 无 用户 id 为生成报告确定"用户是谁"
get_current_month 无 月份字符串 为生成报告确定时间范围
fetch_external_data user_id, month 使用记录字符串/空串 从外部 CSV 取指定用户某月使用记录
fill_context_for_report 无 一句话 占位工具,调用即触发提示词切换(下集实现)

其中最有技术含量的是 fetch_external_data 背后的外部数据组织 。数据来自 data/external/records.csv,记录了每个用户每个月份的使用特征、效率、对比等字段。课程的做法是写一个 generate_external_data(),把 CSV 读成一个两级嵌套字典 :{user_id: {month: {"feature":..., "efficiency":..., "comparison":...}}}。

几个关键工程处理:

  • 用一个全局空字典 external_data = {} 做缓存,if not external_data: 只在第一次时填充,避免工具被多次调用时反复读文件;

  • 路径经过 get_abs_path 转换,读前 os.path.exists 校验,不存在就抛 FileNotFoundError;

  • 打开文件后用 f.readlines()[1:]切片跳过表头 ,再逐行 split(','),并用 .replace('"', '') 去掉 CSV 里的双引号;

  • fetch_external_data 内部先调一次 generate_external_data() 保证字典已建好,再 try: return external_data[user_id][month],except KeyError 时 log.warning(...) 并返回空字符串------查不到不报错,而是优雅返回空串让模型自己判断。

这一集还有一个高频踩坑点 :函数一旦加上 @tool,它就不再是普通函数了,不能像以前那样直接 fetch_external_data("1001", "2025-01") 来测试(因为调用方式改由 Agent 驱动)。课程的解决办法是测试时先把 @tool 注释掉、直接调用 generate_external_data 或裸函数验证逻辑,测完务必把装饰器加回来。

相关推荐
程序员大阳1 小时前
副队长CSS教程(36)–移动布局与响应式
前端·css·响应式·媒体查询·移动布局
小蒋观天下1 小时前
【安防AI工程实战二】AI摄像头帧率抖动根治:ISP曝光时序与AI推理时序冲突深度定位
人工智能·深度学习·算法·安全·机器学习·ai大模型
程序员Sunday1 小时前
React 的 useEffect 怎么工作?依赖数组、清理函数和重复执行如何理解?
前端·javascript
前端.火鸡1 小时前
大模型概念地图:所有术语分别站在哪一层
人工智能
打工仔折腾 AI1 小时前
鸿蒙平板远控电脑实测:ToDesk蓝牙键鼠、双模式鼠标与跨端剪贴板落地记录
人工智能·后端·python·性能优化·计算机外设·电脑·harmonyos
云和数据.ChenGuang1 小时前
trace 是RAG检索链路的**全链路追踪日志对象
人工智能·深度学习·算法·排序算法
鼓掌MVP1 小时前
决定全案的智能Agent
大数据·人工智能·机器学习
lank_M1 小时前
Word裁剪图片转HTML走样,该在渲染层修还是入口修
前端·图像处理·人工智能·计算机视觉·html·word
IT_陈寒1 小时前
深入理解Redis:核心原理与最佳实践
前端·人工智能·后端