AI学习-从本地 RAG 到生产级 Agent 系统

📚 目录

  1. 什么是 Agent
  2. ReAct 框架:推理-行动循环
  3. 模型如何知道调用工具
  4. Memory 系统
  5. Tool Use 工程实践
  6. Agent 质量的差异与优化
  7. 主流框架选型
  8. 多 Agent 协作系统
  9. 生产级 Agent 工程
  10. 学习路线与推荐资源

1. 什么是 Agent

**Agent(智能体)**是指能够自主感知环境、做出决策并采取行动来达成目标的 AI 系统。它与普通 LLM 调用的根本区别在于:

  • LLM:单次【问题→答案】映射
  • Agent:多步【感知→推理→行动】循环

1.1 RAG vs Agent:

前面已经完成了本地 RAG 系统,这是进入 Agent 世界最好的起点,因为 RAG 的【检索】本质上就是 Agent 最常用的第一个工具。

维度 工作方式 核心差异
RAG 固定流程:用户问题 → 向量检索 → 拼 Prompt → LLM 回答 被动,每次必须检索
Agent 动态决策:LLM 自主判断要不要检索、检索什么、检索几次 主动,按需调用工具

核心认知:Agent 不是新技术,是把 LLM 从「被动回答者」变成「主动执行者」的工程设计模式。

1.2 Agent 的四个核心组件

一个完整的 Agent 系统由四部分构成:

组件 职责 类比
规划(Planning) 决定下一步做什么、如何分解任务 大脑/思维
记忆(Memory) 存储对话历史、检索结果、中间状态 短期/长期记忆
工具(Tools) 搜索、代码执行、数据库查询、API 调用等 手和感官
行动(Action) 执行工具调用、输出结果、与外部世界交互 执行器

2. ReAct 框架:推理-行动循环

**ReAct(Reasoning + Acting)**是目前最主流的 Agent 框架,它让模型在每一步先写出推理过程,再执行工具调用,再观察结果,循环往复直到任务完成。

2.1 三步循环

  1. Thought(思考):模型输出内部推理,分析当前状态、决定下一步
  2. Action(行动):调用具体工具,执行操作
  3. Observation(观察):获得工具返回结果,塞回对话历史

完整的一轮示例:

Thought: 用户问入职两年有几天年假,我需要查公司政策
Action: search_hr_policy(query="年假天数")
Observation: 入职第一年5天,第二年起每年增加1天,最多15天
Thought: 已有足够信息,入职两年=第一年5天+第二年增加1天=6天
Answer: 您入职两年,共有 6 天年假。

2.2 为什么 Thought 步骤至关重要

不加 Thought 的 Agent 容易出现「随机调用工具」的问题——模型不经思考直接调用,导致参数错误、调用时机不对。Thought 步骤的作用:

  • 强迫模型在行动前先推理,降低盲目调用概率
  • 推理过程留在对话历史中,后续步骤可以参考
  • 出错时便于调试,可以看到模型的推理链

工程注意:deepseek-r1 的 <think> 标签本质上是显式的 Thought 步骤。建议在 Agent 场景下保留思考过程,不要全部过滤掉。

2.3 循环终止条件

Agent 循环需要明确的终止条件,否则会无限运行:

  • 模型输出不包含工具调用(msg.tool_calls 为空)
  • 达到最大步数上限(建议 5~10 步)
  • 工具返回特殊的完成信号
  • 检测到循环:连续两步调用相同工具+相同参数

3. 模型如何知道调用工具

这是理解 Function Calling 机制最核心的问题,答案分两层。

3.1 训练层面(根本原因)

现代 LLM 在 RLHF / 指令微调阶段,接触了大量格式化的工具调用训练数据:

用户问题 + 工具列表 → 模型输出 JSON 调用 → 工具返回结果 → 自然语言回答

模型从这些数据里学到了一个行为模式:

  • 如果问题需要外部信息 → 输出工具调用 JSON,而不是直接编造
  • 如果问题可以直接回答 → 输出自然语言
  • 这不是写死的 if-else,是从数据归纳出来的概率判断

3.2 推理层面(每次调用时)

API 收到请求后,将 tools 参数转化为系统 Prompt 注入给模型,大致内容为:

You have access to the following functions:

- search_hr_policy(query: str): 在HR知识库检索政策,当需要...时调用

If you want to call a function, respond ONLY with JSON:
{"name": "search_hr_policy", "arguments": {"query": "..."}}

模型在生成 token 时评估两条路径的概率:

  • 直接回答路径:如果它已知足够信息
  • 工具调用路径:如果需要外部信息且 description 与问题匹配

关键认知tool_choice="auto" 不是框架在判断,是模型自己在判断。API 只做格式转换,判断权在模型。

3.3 Tool Description 决定调用准确率

description 是模型判断「该不该调这个工具」的唯一依据,写法差异对准确率影响显著:

质量 description 内容 效果
差的写法 “查询HR信息” 太模糊,随机调用
一般写法 “查询年假、报销等政策” 稍好,仍有歧义
好的写法 “当需要年假/报销/绩效政策时调用。不适用于闲聊、数学计算或政策已在上文出现的情况” 明确触发+排除条件

好的 description 模板:

[何时调用] 当用户询问...时调用本工具。
[何时不调用] 不适用于...(排除条件)。
[参数说明] query: 检索关键词,应简洁具体,例如"年假天数"而非"公司政策"。

4. Memory 系统

记忆是 Agent 与普通对话机器人最大的差异之一。设计良好的记忆系统决定了 Agent 能处理多复杂的任务。

4.1 四种记忆类型

类型 内容 特点 实现方式
工作记忆 (Working Memory) 当前对话的 messages 列表 容量有限,会话结束即消失 你代码里的 messages 变量
情景记忆 (Episodic Memory) 历史任务的结果、用户偏好记录 持久化存储,跨会话可用 存到数据库/文件
语义记忆 (Semantic Memory) 知识库、文档、事实 静态知识,通过 RAG 检索 你已有的向量库
程序记忆 (Procedural Memory) 常用工作流程、任务模板 固化为 Prompt 或函数 system prompt 里的规则

4.2 上下文窗口管理

工作记忆(messages 列表)是最容易出问题的地方。随着对话轮数增加,token 消耗激增,模型开始忘记早期信息。

常用压缩策略:

  1. 截断策略:工具返回超过 N 字就截断,只保留前 K 条最相关片段
  2. 摘要策略:每隔 N 轮用 LLM 对历史对话生成摘要,替换原始记录
  3. 滑动窗口:始终保留最近 N 条消息 + 初始 system prompt
  4. 分层存储:重要信息提取存入外部数据库,按需检索回来

实用代码片段:

def trim_history(history, max_tokens=4000):
    """超出限制时,保留 system + 最近若干轮"""
    system = [m for m in history if m["role"] == "system"]
    others = [m for m in history if m["role"] != "system"]
    # 从最新往前保留,直到接近 token 上限
    return system + others[-20:]  # 简单版:只保留最近20条

5. Tool Use 工程实践

5.1 工具的四个要素

每个工具必须清晰定义四个要素,缺一不可:

要素 作用 示例/格式 注意点
name 唯一标识符 search_hr_policy 简洁,动词+名词
description 调用时机说明 何时调/何时不调/适用场景 这是准确率的关键
parameters 输入参数的 schema JSON Schema 格式,含 type/description 每个参数都要写 description
function 实际执行函数 Python 函数,返回字符串 错误时返回错误信息字符串

5.2 错误处理:最容易被忽视的细节

工具调用失败时,正确的做法是把错误信息返回给模型,让模型自行决定如何处理:

try:
    result = TOOL_REGISTRY[name](**args)
except Exception as e:
    result = f"工具执行失败:{str(e)}。请尝试换一个查询词,或告知用户无法获取该信息。"

# 把 result(包括错误信息)塞回 messages
messages.append({"role": "tool", "tool_call_id": ..., "content": result})

⚠️ 静默失败最危险:工具报错后不处理,让模型直接生成答案——模型会编造一个「看起来合理」的回答,用户完全无法发现错误。

5.3 工具返回结果的处理

不要把原始工具结果直接塞给模型,先做清洗:

  • 截断过长内容(超过 500 字的 RAG 结果只取前几条最相关的)
  • 去除噪声(HTML 标签、多余空白、重复内容)
  • 结构化标注(“以下是检索结果:” 比直接粘贴更清晰)
  • 标注置信度(如果检索得分很低,可以告诉模型"结果相关度较低")

5.4 本地模型的 Function Calling 兼容性

不同模型对 Function Calling 的支持程度差异很大:

模型 FC 支持 备注
qwen2.5:7b ✅ 优秀 推荐首选,中文+工具调用都强
llama3.1:8b ✅ 良好 英文场景更稳定
deepseek-r1:7b ⚠️ 一般 有 think 标签干扰,参数解析偶尔失败
mistral:7b ❌ 较弱 需要手动 Prompt 引导才能稳定调用

6. Agent 质量的差异与优化

同样的基础模型,不同的工程设计可能带来质量上的巨大差距。差距主要体现在以下维度:

6.1 四个核心质量维度

维度 影响 优化方向
工具描述质量 影响工具调用的准确率和时机 description 加上负面示例
上下文管理 影响长任务的稳定性和记忆能力 截断/摘要/分层存储
错误恢复 影响任务完成率和鲁棒性 把错误塞回 messages 让模型决策
任务分解 影响复杂任务的处理质量 先让模型输出计划再逐步执行

6.2 任务分解:处理复杂任务的关键

对于需要多步骤的复杂任务,直接扔给 Agent 效果往往很差。正确做法是先让模型规划再执行:

在 system prompt 里加入规划指令:

system: |
  对于复杂问题(涉及多个政策/多个步骤),请先输出执行计划:
  1. 列出需要查询哪些信息
  2. 列出执行顺序
  再按计划逐步执行,每步完成后确认结果再继续。

人机协作:复杂任务建议加入「人工检查点」——Agent 完成规划后先展示给用户确认,再执行。这是生产系统中最常用的安全设计。

6.3 常见失败模式与应对

失败模式 表现 应对方法
无限循环 同一工具被反复调用 检测「连续相同调用」,自动跳出
幻觉工具调用 调用不存在的工具名 TOOL_REGISTRY.get(name) 做校验
参数错误 JSON 解析失败 用 try/except 包裹 json.loads,失败时让模型重试
上下文遗忘 长对话中忘记早期信息 摘要策略 + 关键信息单独存储
过度工具调用 不需要检索时也检索 description 里明确写排除条件
静默失败 工具报错模型自行编造 所有工具调用加 try/except,错误信息显式返回

7. 主流框架选型

选择框架不是越强大越好,而是与你的任务复杂度匹配。

7.1 框架对比

框架 核心设计 优势 适用场景
原生 Function Calling 完全自己写循环 极简,无依赖 学习原理最佳;维护成本高
LangGraph 状态机 + 节点图 灵活,可视化流程 复杂任务;生产推荐
LlamaIndex Workflows 事件驱动 DAG 与 RAG 集成好 RAG+Agent 融合场景
AutoGen 多 Agent 对话 多角色协作自然 需要多 Agent 讨论的任务
CrewAI 角色驱动多 Agent 上手快,配置简单 快速原型,有角色分工

7.2 你现在的代码应该如何演进

基于你已有的 RAG + 原生 Function Calling 代码,建议的演进路径:

  1. 当前阶段(已完成):原生 Function Calling + 手写 Agent 循环
  2. 下一步:引入 LangGraph,把 Agent 循环改造成状态图,支持条件跳转和并行
  3. 再下一步:多 Agent 协作——至少两个专职 Agent(检索 Agent + 写作 Agent)
  4. 生产阶段:加入监控、日志、限流、人工检查点

8. 多 Agent 协作系统

当单个 Agent 的能力触及上限(上下文太长、任务太复杂、需要专业分工),就需要引入多 Agent 协作。

8.1 三种主要协作模式

主从模式(Orchestrator-Worker)

一个主控 Agent 负责规划和分配,多个工作 Agent 负责执行具体子任务。最常用,适合任务有明确分工的场景。

主控 Agent: 接受用户任务 → 分解 → 分配给专职 Worker
    HR Worker:  只负责查询公司政策
    Writer Worker: 只负责生成报告
主控 Agent: 汇总结果 → 返回用户
流水线模式(Pipeline)

多个 Agent 串联,每个 Agent 的输出是下一个的输入。适合有固定处理步骤的场景,如:搜集资料 → 分析 → 撰写报告。

对等协作模式(Peer Collaboration)

多个 Agent 平等对话,通过讨论达成共识。适合需要多角度验证的任务,如代码 Review、方案评估。

8.2 多 Agent 的核心挑战

  • 通信开销:Agent 之间传递信息需要 token,多轮协作后成本急剧上升
  • 一致性:多个 Agent 可能基于不同信息得出矛盾结论
  • 调试难度:单 Agent 失败容易定位,多 Agent 失败很难追踪
  • 死锁:Agent A 等 Agent B 的结果,Agent B 等 Agent A

建议:先把单 Agent 做稳,再引入多 Agent。过早引入多 Agent 会放大所有工程问题,而不是解决它们。


9. 生产级 Agent 工程

9.1 评估体系

没有评估体系的 Agent 是无法改进的。建立评估体系需要三个层次:

  1. 工具调用准确率:工具被正确调用的比例(调用时机对 + 参数正确)
  2. 任务完成率:Agent 成功完成用户意图的比例
  3. 用户满意度:最终答案的质量评分

最小可行的评估方案:

  • 准备 20~50 个典型问题 + 期望答案(Golden Set)
  • 跑 Agent,对比实际输出 vs 期望输出
  • 记录每次工具调用日志,分析失败原因
  • 每次优化后重新跑 Golden Set,看指标变化

9.2 可观测性:日志与追踪

生产 Agent 必须记录每一步的输入输出,否则出问题无从排查:

import logging, time

def traced_tool_call(name, args, fn):
    t0 = time.time()
    logging.info(f"[TOOL] {name} | args={args}")
    try:
        result = fn(**args)
        logging.info(f"[TOOL] {name} | ok | {time.time()-t0:.2f}s")
        return result
    except Exception as e:
        logging.error(f"[TOOL] {name} | error: {e}")
        return f"执行失败:{e}"

9.3 安全与控制

Agent 的自主性越高,安全风险越大。生产系统必须设置明确的护栏:

安全措施 说明
工具权限最小化 只暴露任务必要的工具,不给 Agent 文件系统写权限除非必须
操作确认机制 破坏性操作(删除、发送邮件、转账)必须人工确认
步数硬限制 设置 max_steps 上限(建议 10 步),超出自动终止并报警
输出过滤 Agent 输出在返回用户前,检查是否包含敏感信息
速率限制 限制单用户单位时间的 Agent 调用次数,防止滥用

9.4 MCP 协议:工具生态的标准化

**Model Context Protocol(MCP)**是 Anthropic 发布的开放协议,旨在标准化 LLM 与外部工具/数据源的连接方式。

MCP 的意义:之前每个 Agent 框架都有自己的工具接口格式,导致工具无法复用。MCP 提供统一接口规范,工具开发者写一次,所有支持 MCP 的 Agent 框架都能用。

  • 本地工具(文件系统、Shell):直接用 MCP 本地服务器
  • 云服务工具(GitHub、Slack、数据库):通过 MCP 远程服务器
  • Claude Desktop / Claude Code:已原生支持 MCP

10. 学习路线与推荐资源

10.1 四阶段学习路线

阶段 主题 核心内容 里程碑
阶段一 (2~3 周) 理解机制 ReAct 框架、Function Calling 原理、Memory 类型 能用原生 API 写出基础 Agent 循环
阶段二 (3~4 周) 动手实践 LangGraph / LlamaIndex、Agentic RAG、Tool Use 实战 能独立搭建研究助手 Agent
阶段三 (3~4 周) 多 Agent AutoGen / CrewAI、协作模式、任务分解规划 能构建两个 Agent 协作完成复杂任务
阶段四 (持续) 工程化 评估体系、可观测性、安全、MCP 协议、部署 能把 Agent 系统上线并稳定运行

10.2 你的下一步行动

基于你已有的 RAG 代码,最高效的下一步是:

  1. 改造现有 RAG:把 search_hr_policy 包装成 Tool,加上 run_agent 循环(已完成)
  2. 加第二个工具:比如 get_employee_info(name) 查员工信息,体验多工具选择
  3. 引入 LangGraph:把 Agent 循环改写为状态图,理解状态管理的优势
  4. 做 Agentic RAG:让 Agent 自主决定「是否检索」「检索几次」「用哪个关键词」
  5. 构建评估集:准备 30 个问答对,建立自动评估管道,量化优化效果

10.3 推荐参考资料

论文

文档

课程

  • DeepLearning.AI: AI Agents in LangGraph(推荐)
  • DeepLearning.AI: Multi AI Agent Systems with crewAI
  • Hugging Face Agents Course(免费,中文社区有译版)

🎯 最后一句话

Agent 工程 90% 的差距来自工程质量(工具描述、错误处理、上下文管理),而不是换更大的模型。先把基础做扎实,再谈复杂架构。


📝 附录:完整代码清单

主要依赖

pip install openai langchain-community chromadb sentence-transformers rank-bm25

启动本地 LLM(Ollama 示例)

ollama run qwen3.5:9b

运行测试

python agent_demo.py

核心代码结构速查

# ============================================================
# 从你的 RAG 升级到 Agent —— 最小改动版
# 核心思路:把 rag_answer 变成 Tool,让 LLM 自己决定调不调
# ============================================================

import json
import re
from openai import OpenAI

# ── 1. 你原有的知识库和检索器(保持不变)──────────────────────
documents = [
    "公司年假政策:入职第一年享有5天年假,第二年起每年增加1天,最多15天。",
    "报销流程:员工需在费用发生后30天内提交报销申请,超过30天不予报销。",
    "远程办公规定:每周最多可申请3天远程办公,需提前一天向直属上级报备。",
    "绩效考核:每年12月进行年度考核,分为A/B/C/D四个等级,A级员工获得20%奖金。",
    "培训补贴:员工参加与工作相关的培训课程,公司报销最高3000元/年的培训费用。",
]

from langchain_community.retrievers import BM25Retriever
from langchain_community.vectorstores import Chroma
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain.retrievers import EnsembleRetriever

embeddings = HuggingFaceEmbeddings(model_name="shibing624/text2vec-base-chinese")
vectorstore = Chroma.from_texts(texts=documents, embedding=embeddings)

bm25_retriever = BM25Retriever.from_texts(documents)
bm25_retriever.k = 3
vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
retriever = EnsembleRetriever(
    retrievers=[bm25_retriever, vector_retriever],
    weights=[0.4, 0.6]
)


# ── 2. 把检索逻辑封装成 Tool 函数 ──────────────────────────────
def search_hr_policy(query: str) -> str:
    """在公司HR知识库中检索相关政策"""
    docs = retriever.invoke(query)
    if not docs:
        return "未找到相关政策。"
    return "\n".join([f"- {d.page_content}" for d in docs])


def get_current_date() -> str:
    """获取今天的日期(用于计算入职年限等)"""
    from datetime import date
    return str(date.today())


# ── 3. 定义 Tool Schema(告诉 LLM 有哪些工具可用)──────────────
TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "search_hr_policy",
            "description": "在公司HR知识库中检索政策文件,当需要回答关于年假、报销、绩效、培训等公司制度问题时调用",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "description": "检索关键词,例如:'年假天数'、'报销时限'",
                    }
                },
                "required": ["query"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "get_current_date",
            "description": "获取今天的日期,当问题涉及时间计算时使用",
            "parameters": {"type": "object", "properties": {}},
        },
    },
]

# Tool 名称 → 实际函数的映射
TOOL_REGISTRY = {
    "search_hr_policy": search_hr_policy,
    "get_current_date": get_current_date,
}


# ── 4. 连接本地 LLM(你原有的配置)───────────────────────────────
client = OpenAI(
    api_key="ollama",
    base_url="http://localhost:11500/v1"
)
MODEL = "qwen3.5:9b"


def strip_think(text: str) -> str:
    """去掉 deepseek-r1 的 <think> 标签"""
    return re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL).strip()


# ── 5. Agent 循环:核心升级点 ──────────────────────────────────
def run_agent(user_question: str, max_steps: int = 5) -> str:
    """
    ReAct 风格的 Agent 主循环:
      1. LLM 看到问题,决定调用哪个 Tool
      2. 执行 Tool,把结果返回给 LLM
      3. LLM 再决定:继续调 Tool,还是直接回答
      4. 重复直到 LLM 给出最终答案(或达到步数上限)
    """
    messages = [
        {
            "role": "system",
            "content": (
                "你是公司的HR助手。回答员工问题时,"
                "必须先调用 search_hr_policy 检索相关政策,"
                "只根据检索到的内容回答,不要编造信息。"
            ),
        },
        {"role": "user", "content": user_question},
    ]

    print(f"\n{'='*55}")
    print(f"问题:{user_question}")

    for step in range(max_steps):
        # 请求 LLM(携带 tools 参数)
        response = client.chat.completions.create(
            model=MODEL,
            messages=messages,
            tools=TOOLS,
            tool_choice="auto",   # 让 LLM 自己决定用不用工具
        )
        msg = response.choices[0].message

        # ── 情况A:LLM 决定调用工具 ──────────────────────────
        if msg.tool_calls:
            # 把 LLM 的决策加入对话历史
            messages.append(msg)

            for tool_call in msg.tool_calls:
                name = tool_call.function.name
                args = json.loads(tool_call.function.arguments)

                print(f"\n[步骤{step+1}] 🔧 调用工具:{name}({args})")

                # 执行实际函数
                result = TOOL_REGISTRY[name](**args)
                print(f"         📄 工具返回:{result[:100]}...")  # 截断显示

                # 把工具结果放回对话,LLM 下一轮能看到
                messages.append({
                    "role": "tool",
                    "tool_call_id": tool_call.id,
                    "content": result,
                })

        # ── 情况B:LLM 直接给出最终回答 ──────────────────────
        else:
            answer = strip_think(msg.content or "")
            print(f"\n[最终回答] 💬 {answer}")
            print(f"{'='*55}")
            return answer

    return "超过最大步数,未能完成回答。"


# ── 6. 测试运行 ────────────────────────────────────────────────
if __name__ == "__main__":
    questions = [
        "我入职两年了,有几天年假?",
        "报销需要多少天内提交?",
        "绩效A级有什么奖励?",
        # 下面这个问题会让 Agent 组合调用两个工具
        "今天是几号?年底考核还有多久?",
    ]

    for q in questions:
        run_agent(q)

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐