Agent开发入门,看这篇就够了

4 阅读24分钟

LangChain 环境搭建指南

目标

搭建 Python 开发环境,安装 LangChain,运行第一个程序。


步骤 1:创建项目目录

mkdir -p /Users/zhs/MyFiles/projects/test/test-agent/langchain-learn
cd /Users/zhs/MyFiles/projects/test/test-agent/langchain-learn

步骤 2:创建 Python 虚拟环境

python3 -m venv venv

虚拟环境会创建在项目目录下的 venv/ 文件夹中。


步骤 3:安装依赖包

激活虚拟环境并安装依赖:

# 激活虚拟环境
source venv/bin/activate

# 安装核心依赖
pip install langchain langchain-community dashscope python-dotenv

已安装的依赖:

包名版本作用
langchain最新LangChain 核心框架
langchain-community最新社区模型支持(含通义千问)
dashscope最新阿里云 DashScope SDK
python-dotenv最新加载 .env 环境变量

步骤 4:获取通义千问 API Key

  1. 访问阿里云 DashScope 控制台:dashscope.console.aliyun.com/apiKey
  2. 注册/登录阿里云账号(新用户有免费额度)
  3. 点击「创建 API Key」
  4. 复制生成的 Key(格式:sk-xxxxxxxxxx

步骤 5:配置环境变量

# 复制配置模板
cp .env.example .env

# 编辑 .env 文件(用文本编辑器)
open -e .env

将文件内容修改为:

DASHSCOPE_API_KEY=sk-你的真实API-Key

步骤 6:运行第一个程序

# 确保在虚拟环境中
source venv/bin/activate

# 运行程序
python 01_hello_langchain.py

预期输出:

==================================================
基础用法 1:直接字符串调用
==================================================
AI 回答: LangChain 是一个用于开发大语言模型应用的框架...

==================================================
基础用法 2:使用消息列表
==================================================
AI 回答:
1. ...
2. ...
3. ...

==================================================
基础用法 3:流式输出
==================================================
AI 回答: Agent 是...

==================================================
🎉 恭喜!你已经完成了第一个 LangChain 程序!
==================================================

常见问题

Q1: 提示 "DASHSCOPE_API_KEY not found"

解决:检查 .env 文件是否正确创建,且 Key 格式正确。

Q2: 提示模型调用失败

解决

  1. 检查 API Key 是否有效
  2. 检查阿里云账号是否有免费额度
  3. 确认网络连接正常

Q3: 如何退出虚拟环境

deactivate

二、 LLM 基础调用指南

目标

掌握 LangChain 中 LLM 的基本使用,包括消息类型、提示词模板、输出解析器。


前置要求

  • 已完成 Task 1(环境搭建)
  • 虚拟环境已激活
  • API Key 配置正确

步骤 1:理解核心概念

1.1 Chat Models vs LLMs

类型说明使用场景
Chat Models对话模型,接收消息列表大多数场景(推荐)
LLMs文本补全模型,接收纯文本特定补全任务

我们使用 Chat Models(如通义千问)

1.2 消息类型

from langchain_core.messages import SystemMessage, HumanMessage, AIMessage

# SystemMessage: 设置 AI 角色和行为
system_msg = SystemMessage(content="你是一个专业翻译")

# HumanMessage: 用户输入
human_msg = HumanMessage(content="把这句话翻译成英文")

# AIMessage: AI 回复(通常用于历史记录)
ai_msg = AIMessage(content="This is the translation")

步骤 2:创建示例代码文件

创建文件 02_llm_basics.py

touch /Users/zhs/MyFiles/projects/test/test-agent/langchain-learn/02_llm_basics.py

步骤 3:基础调用练习

3.1 直接字符串调用

from langchain_community.chat_models import ChatTongyi

llm = ChatTongyi(model="qwen-turbo")

# 方式1:直接传字符串
response = llm.invoke("什么是机器学习?")
print(response.content)

3.2 使用消息列表(推荐)

from langchain_core.messages import SystemMessage, HumanMessage

messages = [
    SystemMessage(content="你是一位耐心的老师,用简单语言解释概念。"),
    HumanMessage(content="什么是机器学习?")
]

response = llm.invoke(messages)
print(response.content)

步骤 4:提示词模板(PromptTemplate)

4.1 基础模板

from langchain_core.prompts import ChatPromptTemplate

# 创建模板
template = ChatPromptTemplate.from_messages([
    ("system", "你是一位{role},擅长{skill}。"),
    ("human", "请帮我{task}")
])

# 填充变量
messages = template.format_messages(
    role="Python专家",
    skill="数据分析",
    task="写一个读取CSV的代码"
)

response = llm.invoke(messages)
print(response.content)

4.2 链式调用(LCEL)

# 使用管道符 | 构建链
chain = template | llm

# 直接传入变量
response = chain.invoke({
    "role": "Python专家",
    "skill": "数据分析",
    "task": "写一个读取CSV的代码"
})

print(response.content)

步骤 5:输出解析器(OutputParser)

5.1 字符串输出解析

from langchain_core.output_parsers import StrOutputParser

# 创建解析器
parser = StrOutputParser()

# 构建链:模板 -> LLM -> 解析器
chain = template | llm | parser

# 获取纯字符串输出(不含元数据)
result = chain.invoke({
    "role": "Python专家",
    "skill": "数据分析",
    "task": "写一个读取CSV的代码"
})

print(result)  # 纯字符串

5.2 JSON 结构化输出

from langchain_core.output_parsers import JsonOutputParser
from langchain_core.pydantic_v1 import BaseModel, Field

# 定义输出结构
class Person(BaseModel):
    name: str = Field(description="姓名")
    age: int = Field(description="年龄")
    hobbies: list = Field(description="爱好列表")

# 创建解析器
parser = JsonOutputParser(pydantic_object=Person)

# 创建提示词模板
template = ChatPromptTemplate.from_template("""
提取以下文本中的信息:
{text}

{format_instructions}
""")

# 构建链
chain = template.partial(format_instructions=parser.get_format_instructions()) | llm | parser

# 调用
result = chain.invoke({
    "text": "张三今年25岁,喜欢打篮球和编程。"
})

print(result)  # {'name': '张三', 'age': 25, 'hobbies': ['打篮球', '编程']}

步骤 6:流式输出

# 流式输出,适合长文本
print("AI 回答: ", end="")
for chunk in llm.stream("写一个100字的自我介绍"):
    print(chunk.content, end="", flush=True)
print()

完整练习代码

我已经为你准备了完整的练习代码,包含以上所有内容。

运行方式:

cd /Users/zhs/MyFiles/projects/test/test-agent/langchain-learn
source venv/bin/activate
python 02_llm_basics.py

练习任务

完成以下任务巩固学习:

练习 1:多语言翻译器

创建一个支持多种语言翻译的提示词模板。

练习 2:信息提取器

从一段文本中提取:人名、地点、时间、事件。

练习 3:代码解释器

输入一段代码,AI 解释其功能和关键步骤。


常见问题

Q1: 提示词模板变量怎么写?

使用 {variable_name} 格式,调用时传入字典。

Q2: 输出解析失败怎么办?

  • 检查模型输出是否符合预期格式
  • 尝试使用更强大的模型(如 qwen-plus)
  • 在提示词中明确要求 JSON 格式

Q3: 流式输出和普通输出区别?

  • 普通输出:等待完整响应后返回
  • 流式输出:逐字返回,用户体验更好

三、 Chain 链式调用指南

目标

理解 Chain 的概念,掌握 LCEL(LangChain Expression Language)语法,学会组合多个步骤。


前置要求

  • 已完成 Task 2(LLM 基础调用)
  • 理解 PromptTemplate 和 OutputParser

核心概念:LCEL

LCEL = LangChain Expression Language,用 | 管道符像拼积木一样组合组件。

# 基本语法
chain = 组件A | 组件B | 组件C

# 例如
chain = prompt | llm | parser
result = chain.invoke({"key": "value"})

步骤 1:Runnable 接口

LangChain 中所有组件都实现 Runnable 接口,支持统一的操作方法:

方法说明
invoke()单次调用,传入单个输入
batch()批量调用,传入列表
stream()流式输出
ainvoke()异步调用

步骤 2:简单链(RunnableSequence)

from langchain_core.prompts import ChatPromptTemplate
from langchain_community.chat_models import ChatTongyi
from langchain_core.output_parsers import StrOutputParser

# 定义组件
prompt = ChatPromptTemplate.from_template("把以下翻译成英文:{text}")
llm = ChatTongyi(model="qwen-turbo")
parser = StrOutputParser()

# 用 | 串联成链
chain = prompt | llm | parser

# 调用
result = chain.invoke({"text": "你好,世界"})
print(result)  # Hello, world

步骤 3:并行执行(RunnableParallel)

同时执行多个任务,结果合并:

from langchain_core.runnables import RunnableParallel

# 定义两个并行的链
chain_a = ChatPromptTemplate.from_template("总结以下内容:{text}") | llm | parser
chain_b = ChatPromptTemplate.from_template("提取关键词:{text}") | llm | parser

# 并行执行
parallel_chain = RunnableParallel(
    summary=chain_a,
    keywords=chain_b
)

result = parallel_chain.invoke({"text": "人工智能正在改变我们的生活..."})
print(result)
# {
#   "summary": "人工智能对生活的影响...",
#   "keywords": "人工智能, 生活, 改变"
# }

步骤 4:条件分支(RunnableBranch)

根据条件选择不同的处理路径:

from langchain_core.runnables import RunnableBranch

# 定义不同场景的链
code_chain = ChatPromptTemplate.from_template("解释这段代码:{input}") | llm | parser
qa_chain = ChatPromptTemplate.from_template("回答问题:{input}") | llm | parser

# 条件分支
branch = RunnableBranch(
    (lambda x: "代码" in x["input"], code_chain),  # 如果包含"代码",走 code_chain
    (lambda x: "问题" in x["input"], qa_chain),    # 如果包含"问题",走 qa_chain
    qa_chain  # 默认分支
)

result = branch.invoke({"input": "这段代码是什么意思:print('hello')"})

步骤 5:自定义函数(RunnableLambda)

在链中插入自定义 Python 函数:

from langchain_core.runnables import RunnableLambda

# 定义自定义函数
def add_hello(text: str) -> str:
    return f"你好,{text}"

# 包装成 Runnable
hello_step = RunnableLambda(add_hello)

# 插入到链中
chain = prompt | hello_step | llm | parser

步骤 6:链的组合与嵌套

复杂场景可以组合多个链:

# 链1:生成内容
generate_chain = (
    ChatPromptTemplate.from_template("写一篇关于{topic}的短文")
    | llm 
    | parser
)

# 链2:翻译内容
translate_chain = (
    ChatPromptTemplate.from_template("把以下内容翻译成英文:{text}")
    | llm 
    | parser
)

# 组合:先生成,再翻译
full_chain = (
    generate_chain 
    | (lambda x: {"text": x})  # 转换格式
    | translate_chain
)

result = full_chain.invoke({"topic": "人工智能"})

完整练习代码

运行方式:

cd /Users/zhs/MyFiles/projects/test/test-agent/langchain-learn
source venv/bin/activate
python 03_chain_lcel.py

练习任务

练习 1:多语言翻译器

创建一个链,支持中文 → 英文 → 日文的多步翻译。

练习 2:智能客服路由

根据用户问题类型(订单/售后/咨询),路由到不同的处理链。

练习 3:内容生成流水线

构建链:主题 → 生成大纲 → 生成内容 → 润色 → 输出。


常见问题

Q1: 链中报错怎么调试?

解决:逐步测试每个组件,确保中间输出格式正确。

Q2: 如何查看链的中间结果?

解决:使用 RunnablePassthrough 或分步调用。

Q3: 链的性能优化?

解决:使用 batch() 批量处理,或启用异步 ainvoke()


四、Memory 对话记忆指南

目标

让 AI 具备上下文记忆能力,实现多轮对话。


前置要求

  • 已完成 Task 3(Chain 链式调用)
  • 理解 LCEL 语法
  • 注意:本教程基于 LangChain 0.2+ 版本(API 有较大变化)

为什么需要 Memory?

默认情况下,LLM 是无状态的,每次调用都是独立的:

# 第一次调用
llm.invoke("我叫张三")  # AI: 你好张三

# 第二次调用
llm.invoke("我叫什么名字?")  # AI: 我不知道(因为没有记忆)

Memory 解决这个问题,让 AI 记住对话历史。


核心概念

1. 消息类型回顾

from langchain_core.messages import SystemMessage, HumanMessage, AIMessage

messages = [
    SystemMessage(content="你是助手"),      # 系统设定
    HumanMessage(content="你好"),           # 用户说
    AIMessage(content="你好!有什么可以帮你?"),  # AI 回复
    HumanMessage(content="我叫张三"),       # 用户说
    AIMessage(content="你好张三!"),         # AI 回复
]

2. Memory 的作用

自动管理消息历史,决定:记住什么、怎么记住、记住多少


步骤 1:ChatMessageHistory(基础消息历史)

LangChain 0.2+ 使用 ChatMessageHistory 管理对话历史。

from langchain_community.chat_message_histories import ChatMessageHistory

# 创建消息历史
history = ChatMessageHistory()

# 添加消息
history.add_user_message("我叫张三")
history.add_ai_message("你好张三!")

# 查看消息
for msg in history.messages:
    print(f"{msg.type}: {msg.content}")
# 输出:
# human: 我叫张三
# ai: 你好张三!

步骤 2:RunnableWithMessageHistory(自动记忆)

LangChain 0.2+ 推荐使用 RunnableWithMessageHistory 包装链,自动处理记忆。

from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_community.chat_models import ChatTongyi
from langchain_core.output_parsers import StrOutputParser
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory

# 创建带记忆的提示词模板
prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一个友好的助手,记住用户的信息。"),
    MessagesPlaceholder(variable_name="history"),  # 历史消息插入位置
    ("human", "{input}")
])

# 初始化
llm = ChatTongyi(model="qwen-turbo")
chain = prompt | llm | StrOutputParser()

# 存储会话历史的字典
store = {}

def get_session_history(session_id: str) -> ChatMessageHistory:
    """获取或创建会话历史"""
    if session_id not in store:
        store[session_id] = ChatMessageHistory()
    return store[session_id]

# 包装成带记忆的链
chain_with_history = RunnableWithMessageHistory(
    chain,
    get_session_history,
    input_messages_key="input",
    history_messages_key="history",
)

# 调用(使用 session_id 区分不同用户)
response = chain_with_history.invoke(
    {"input": "我叫张三"},
    config={"configurable": {"session_id": "user_001"}}
)
print(response)

# 再次调用,AI 能记住名字
response = chain_with_history.invoke(
    {"input": "我叫什么名字?"},
    config={"configurable": {"session_id": "user_001"}}  # 相同的 session_id
)
print(response)  # AI: 你叫张三

步骤 3:滑动窗口记忆(自定义实现)

只保留最近 N 条消息,节省 Token。

class WindowChatMessageHistory(ChatMessageHistory):
    """带窗口限制的消息历史"""
    def __init__(self, max_messages=4, **kwargs):
        super().__init__(**kwargs)
        self._max_messages = max_messages
    
    def add_message(self, message):
        super().add_message(message)
        # 只保留最近 max_messages 条
        if len(self.messages) > self._max_messages:
            self.messages = self.messages[-self._max_messages:]

# 使用
window_history = WindowChatMessageHistory(max_messages=4)

步骤 4:摘要记忆(Summary Memory)

当对话过长时,对历史进行摘要,而不是完整保存。

from langchain_core.messages import SystemMessage

class SummaryChatMessageHistory(ChatMessageHistory):
    """带摘要功能的消息历史"""
    def __init__(self, llm, max_messages=6, **kwargs):
        super().__init__(**kwargs)
        self._llm = llm  # 用于生成摘要的 LLM
        self._max_messages = max_messages
        self._summary = ""
    
    def add_message(self, message):
        super().add_message(message)
        # 当消息超过阈值时,进行摘要
        if len(self.messages) > self._max_messages:
            self._summarize()
    
    def _summarize(self):
        """对历史消息进行摘要"""
        # 取前一半消息进行摘要
        messages_to_summarize = self.messages[:self._max_messages//2]
        
        # 构建摘要提示
        summary_prompt = f"请对以下对话进行简洁摘要:\n{messages_to_summarize}"
        
        # 调用 LLM 生成摘要
        summary_response = self._llm.invoke(summary_prompt)
        self._summary = summary_response.content
        
        # 保留后半部分消息,并在开头添加摘要
        self.messages = self.messages[self._max_messages//2:]
        system_msg = SystemMessage(content=f"【历史摘要】{self._summary}")
        self.messages.insert(0, system_msg)

三种 Memory 对比

Memory 类型特点适用场景Token 消耗
基础记忆完整保存所有消息对话轮数少
滑动窗口只保留最近 N 条只关心近期对话
摘要记忆LLM 自动摘要历史长对话,需了解全貌

实际好处详解

滑动窗口记忆的好处

1. 节省 Token 成本

场景:客服机器人,用户已对话 50 轮
- 基础记忆:发送 50 轮历史 = 消耗 3000+ Token
- 滑动窗口(k=4):只发送最近 4 轮 = 消耗 400 Token
- 节省:86% 的 Token 费用

2. 提高响应速度

  • 每次请求携带的历史越少,API 响应越快
  • 减少网络传输时间

3. 避免无关信息干扰

用户早期说:"我喜欢红色"
用户后期讨论:技术问题

→ 滑动窗口自动"遗忘"颜色偏好,避免 AI 在技术回答中突然提到红色

适用场景

  • 实时客服(只关心当前问题)
  • 代码助手(只关心当前代码上下文)
  • 翻译助手(只关心当前句子)

摘要记忆的好处

1. 兼顾长历史与 Token 成本

场景:医疗问诊,已对话 30 轮
- 基础记忆:30 轮完整对话 = 5000 Token
- 滑动窗口:丢失早期症状描述
- 摘要记忆:
  【摘要】患者男,35岁,3天前发热,昨日咳嗽,体温最高39度...
  + 最近 4 轮详细对话 = 800 Token

2. 保留关键信息,丢弃细节噪音

原始对话(10轮):
- 用户:你好
- AI:你好
- 用户:我想订机票
- AI:好的,去哪里
- 用户:北京
- AI:什么时候
- 用户:下周一
- AI:几点
- 用户:上午
- AI:好的...

摘要结果:
【摘要】用户要订下周一上午去北京的机票

→ 保留了核心意图,去除了寒暄和确认过程

3. 支持超长对话

  • 理论上可以支持无限轮对话
  • 摘要可以再次摘要(分层摘要)

适用场景

  • 医疗问诊(需要了解病史全貌)
  • 法律助手(需要了解案件背景)
  • 小说创作(需要记住人物设定和剧情)
  • 项目管理(需要了解项目历史决策)

如何选择?
场景推荐方案原因
简单问答机器人基础记忆对话短,Token 够用
实时客服滑动窗口只关心当前问题
医疗/法律咨询摘要记忆需要历史全貌
个人助手摘要记忆长期陪伴,记住偏好
代码助手滑动窗口代码上下文变化快

步骤 5:多用户会话隔离

使用不同的 session_id 实现多用户隔离:

# 用户 A
response_a = chain_with_history.invoke(
    {"input": "我叫王五"},
    config={"configurable": {"session_id": "user_A"}}
)

# 用户 B(完全不同的会话)
response_b = chain_with_history.invoke(
    {"input": "我叫赵六"},
    config={"configurable": {"session_id": "user_B"}}
)

# 用户 A 再次询问 - 能记住自己是王五
response_a2 = chain_with_history.invoke(
    {"input": "我叫什么名字?"},
    config={"configurable": {"session_id": "user_A"}}
)

LangChain 0.1 vs 0.2 API 对比

功能0.1.x 旧版本0.2+ 新版本
消息历史ConversationBufferMemoryChatMessageHistory
自动记忆链ConversationChainRunnableWithMessageHistory
保存消息memory.save_context()history.add_user_message() / add_ai_message()
加载历史memory.load_memory_variables()history.messages

完整练习代码

运行方式:

cd /Users/zhs/MyFiles/projects/test/test-agent/langchain-learn
source venv/bin/activate
python 04_memory.py

练习任务

练习 1:带记忆的翻译助手

创建一个翻译助手,记住用户的偏好(如专业领域、目标语言)。

练习 2:多角色对话

实现一个系统,AI 根据用户选择的角色(老师/朋友/专家)调整对话风格。

练习 3:持久化记忆

将对话历史保存到文件或数据库,程序重启后仍能恢复。


常见问题

Q1: Token 超限怎么办?

解决:使用滑动窗口记忆(WindowChatMessageHistory),限制历史长度。

Q2: 如何清空记忆?

解决store[session_id].clear() 或重新创建 ChatMessageHistory

Q3: session_id 有什么用?

解决:区分不同用户或不同会话,实现记忆隔离。

Q4: 旧版代码还能用吗?

解决:LangChain 0.2 仍然兼容旧 API,但推荐使用新 API。


五、Tools 与 Agent 指南 ⭐核心

目标

掌握 Agent 的核心概念,学会创建自定义工具,构建能自主决策的 AI Agent。


前置要求

  • 已完成 Task 4(Memory 对话记忆)
  • 理解 LCEL 链式调用
  • 理解 Python 装饰器概念

核心概念

什么是 Agent?

Agent = LLM + 工具 + 决策能力

普通 LLM 只能回答问题,Agent 可以:

  1. 理解用户意图 → 决定做什么
  2. 选择合适工具 → 执行具体操作
  3. 观察执行结果 → 决定下一步
  4. 循环直到完成 → 给出最终答案

ReAct 模式

Agent 的核心思考模式:Reason(推理)+ Act(行动)

用户:北京今天天气怎么样?

Agent 思考:
1. 用户问天气 → 需要调用天气工具
2. 调用 get_weather("北京")
3. 观察结果:晴天,25°C
4. 组织语言回答用户

最终回答:北京今天晴天,25°C,适合出行!

步骤 1:创建自定义 Tool

使用 @tool 装饰器将 Python 函数变成 Agent 可用的工具。

from langchain.tools import tool

@tool
def calculate_bmi(weight: float, height: float) -> str:
    """
    计算 BMI 指数,评估健康状况。
    
    Args:
        weight: 体重(公斤)
        height: 身高(米)
    
    Returns:
        BMI 值和健康建议
    """
    bmi = weight / (height ** 2)
    
    if bmi < 18.5:
        status = "偏瘦"
    elif bmi < 24:
        status = "正常"
    elif bmi < 28:
        status = "偏胖"
    else:
        status = "肥胖"
    
    return f"BMI: {bmi:.1f}{status}"

# 测试工具
print(calculate_bmi.invoke({"weight": 70, "height": 1.75}))
# 输出:BMI: 22.9,正常

Tool 的关键要素

要素说明
函数名工具的名称,Agent 通过名称识别
参数类型必须标注类型(str, int, float 等)
文档字符串最重要,描述工具功能和参数,Agent 靠这个理解工具
返回值工具执行结果,str 格式最佳

步骤 2:创建多个工具

@tool
def get_current_time() -> str:
    """获取当前时间"""
    from datetime import datetime
    return datetime.now().strftime("%Y-%m-%d %H:%M:%S")

@tool
def word_count(text: str) -> str:
    """
    统计文本字数
    
    Args:
        text: 要统计的文本
    """
    return f"字数:{len(text)}"

# 工具列表
tools = [calculate_bmi, get_current_time, word_count]

步骤 3:创建简化版 Agent

本教程使用简化版 Agent 实现,不依赖 create_react_agent 等高级 API,更容易理解 Agent 的核心原理。

3.1 核心思路

Agent = 决策(LLM)+ 执行(工具)

  1. 把可用工具信息告诉 LLM
  2. 让 LLM 决定使用哪个工具,按固定格式返回
  3. 解析 LLM 的决策,执行对应工具
  4. 返回结果给用户

3.2 完整代码

from langchain_community.chat_models import ChatTongyi
from langchain_core.runnables import RunnableLambda

# 初始化模型
llm = ChatTongyi(model="qwen-turbo", temperature=0.7)

def create_simple_agent(tools, llm):
    """创建简化版 Agent"""
    
    # 构建工具描述
    tools_description = "\n".join([
        f"- {tool.name}: {tool.description.split(chr(10))[0]}"
        for tool in tools
    ])
    
    def agent_invoke(inputs):
        question = inputs["input"]
        
        # 让 LLM 决策
        decision_prompt = f"""你是一个智能助手,可以使用以下工具:

{tools_description}

用户问题:{question}

请分析需要使用哪个工具,按以下格式回答:
TOOL|工具名称|参数1=值1|参数2=值2

示例:
- TOOL|calculate_bmi|weight=70|height=1.75
- TOOL|get_current_time
- TOOL|word_count|text=Hello World

如果不需要工具,直接回答:
ANSWER|你的回答内容

你的决策:"""
        
        # 调用 LLM 获取决策
        decision = llm.invoke(decision_prompt).content.strip()
        print(f"决策:{decision}")
        
        # 解析决策
        parts = decision.split("|")
        action = parts[0].strip()
        
        if action == "TOOL" and len(parts) >= 2:
            tool_name = parts[1].strip()
            # 解析参数
            kwargs = {}
            for param in parts[2:]:
                if "=" in param:
                    key, value = param.split("=", 1)
                    kwargs[key.strip()] = value.strip()
            
            # 执行工具
            for tool in tools:
                if tool.name == tool_name:
                    result = tool.invoke(kwargs)
                    return f"[{tool_name}] {result}"
            return f"未找到工具:{tool_name}"
        
        elif action == "ANSWER":
            return parts[1] if len(parts) > 1 else decision
        
        else:
            return decision
    
    return RunnableLambda(agent_invoke)

# 创建 Agent
agent = create_simple_agent(tools, llm)

# 运行
result = agent.invoke({"input": "我的身高1.75米,体重70公斤,BMI是多少?"})
print(result)  # [calculate_bmi] BMI: 22.9,正常,继续保持

3.3 关键概念解析

决策格式设计

我们设计了一个简单的格式让 LLM 返回决策:

格式含义示例
`TOOL工具名参数=值`调用工具`TOOLcalculate_bmiweight=70height=1.75`
`ANSWER回答内容`直接回答`ANSWER我无法获取天气信息`
执行流程
用户问题
    ↓
构建决策提示词(包含工具描述)
    ↓
LLM 思考并返回决策格式
    ↓
解析决策字符串
    ↓
如果是 TOOL → 查找并执行工具 → 返回结果
如果是 ANSWER → 直接返回回答内容
与标准 ReAct Agent 的对比
方案本教程简化版标准 ReAct Agent
依赖仅依赖基础 LangChain需要 langgraph 等扩展
格式简单自定义格式严格的 ReAct 格式
复杂度单步决策,容易理解多步思考-行动循环
适用学习原理、简单任务生产环境、复杂任务

步骤 4:扩展——使用官方 ReAct Agent(可选进阶)

本教程使用简化版 Agent 帮助理解原理。生产环境中,可以使用 LangChain 官方提供的 create_react_agent(需要安装 langgraph):

pip install langgraph
from langgraph.prebuilt import create_react_agent

# 创建官方 ReAct Agent
agent = create_react_agent(llm, tools)

# 运行
result = agent.invoke({"messages": [("human", "我的BMI是多少?体重70,身高1.75")]})

官方实现的优势:

  • 支持多步思考-行动循环
  • 自动处理工具调用和结果观察
  • 更好的错误处理和边界情况

建议先掌握简化版的原理,再过渡到官方实现。

两种方案对比

方案本教程简化版官方 ReAct Agent
依赖基础 LangChain需要 langgraph
学习曲线平缓,容易理解原理较陡,但功能更强大
多步推理单步决策支持多轮思考-行动
适用场景学习、简单任务生产环境、复杂任务

完整练习代码

运行方式:

cd /Users/zhs/MyFiles/projects/test/test-agent/langchain-learn
source venv/bin/activate
python 05_tools_agent.py

练习任务

练习 1:天气查询 Agent

创建一个工具查询天气,Agent 根据用户问题决定是否调用。

练习 2:多功能助手

创建工具:计算器、翻译器、单位转换器,Agent 自动选择使用。

练习 3:结合 Memory 的 Agent

让 Agent 记住用户偏好,在后续对话中使用。


常见问题

Q1: Agent 和 Chain 的区别?

Chain:固定流程,按预设步骤执行
Agent:自主决策,根据情况选择工具和步骤

Q2: 工具文档字符串有多重要?

极其重要! Agent 完全靠文档字符串理解工具功能,写得不清楚会导致 Agent 选错工具。

Q3: Agent 调用工具失败怎么办?

  • 检查工具文档是否清晰
  • 添加错误处理逻辑
  • 设置 handle_parsing_errors=True

六、 RAG 检索增强生成指南

目标

让 AI 基于你的私有数据(文档、知识库)回答问题,实现"企业知识问答"。


前置要求

  • 已完成 Task 5(Tools 与 Agent)
  • 理解 Embedding 和向量数据库基本概念

核心概念:什么是 RAG?

RAG = Retrieval(检索)+ Augmented(增强)+ Generation(生成)

为什么需要 RAG?

LLM 的问题是:

  • ❌ 不知道你的私有数据(公司内部文档、个人笔记)
  • ❌ 知识有截止日期
  • ❌ 可能"幻觉"(编造答案)

RAG 解决思路:

用户提问 → 检索相关知识 → 将知识注入 Prompt → LLM 基于知识回答

RAG 流程图

┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│   用户提问       │────→│  检索相关文档    │────→│  LLM 基于文档   │
│  "公司年假几天?" │     │  (向量相似度)  │     │   生成答案      │
└─────────────────┘     └─────────────────┘     └─────────────────┘
         │                                               │
         │         ┌─────────────────┐                  │
         └────────→│   向量数据库     │←─────────────────┘
                   │  (Chroma/FAISS) │
                   └─────────────────┘
                            ↑
                   ┌─────────────────┐
                   │   文档预处理     │
                   │ 加载→分割→Embedding
                   └─────────────────┘

步骤 1:准备文档

创建测试文档:

mkdir -p /Users/zhs/MyFiles/projects/test/test-agent/langchain-learn/docs

创建 docs/company_policy.txt

公司名称:未来科技有限公司

【考勤制度】
1. 工作时间:周一至周五,9:00-18:00,午休1小时
2. 迟到:每月前3次免责,超过3次每次扣50元
3. 年假:入职满1年享5天,满3年享10天,满5年享15天
4. 请假:需提前1天在系统申请,紧急情况下可后补

【薪资福利】
1. 发薪日:每月10日
2. 五险一金:按全额工资缴纳
3. 年终奖:根据绩效,1-4个月工资
4. 团建:每季度一次,预算人均500元

【报销制度】
1. 差旅:机票经济舱,酒店不超过400元/晚
2. 餐饮:外出就餐每人每餐不超过100元
3. 发票:需在消费后30天内提交

步骤 2:加载和分割文档

from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter

# 加载文档
loader = TextLoader("docs/company_policy.txt", encoding="utf-8")
documents = loader.load()

print(f"加载了 {len(documents)} 个文档")
print(f"文档长度:{len(documents[0].page_content)} 字符")

# 分割文档(避免超过模型上下文限制)
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=200,      # 每块最大字符数
    chunk_overlap=50,    # 块之间重叠字符数(保持上下文连贯)
    separators=["\n\n", "\n", "。", "!", "?"]  # 优先在这些位置分割
)

chunks = text_splitter.split_documents(documents)

print(f"分割成 {len(chunks)} 个片段")
for i, chunk in enumerate(chunks[:3]):
    print(f"\n片段 {i+1}:")
    print(chunk.page_content[:100] + "...")

分割策略说明

参数作用
chunk_size每块最大长度,太小会丢失上下文,太大会超 Token
chunk_overlap块之间重叠,保证语义连贯
separators优先在句子/段落边界分割,避免切断语义

步骤 3:创建向量数据库

from langchain_community.embeddings import DashScopeEmbeddings
from langchain_community.vectorstores import Chroma

# 初始化 Embedding 模型(使用通义千问的 Embedding)
embeddings = DashScopeEmbeddings(
    model="text-embedding-v2",  # 通义千问 Embedding 模型
    dashscope_api_key=os.getenv("DASHSCOPE_API_KEY")
)

# 创建向量数据库
vectorstore = Chroma.from_documents(
    documents=chunks,
    embedding=embeddings,
    persist_directory="./chroma_db"  # 持久化存储位置
)

print("向量数据库创建完成!")
print(f"共存储 {vectorstore._collection.count()} 个向量")

# 测试相似度搜索
query = "年假有多少天?"
results = vectorstore.similarity_search(query, k=2)

print(f"\n查询:{query}")
print("最相关的片段:")
for i, doc in enumerate(results):
    print(f"{i+1}. {doc.page_content[:100]}...")

Embedding 是什么?

将文本转换为高维向量(一串数字),语义相似的文本向量距离近。

"年假" → [0.1, -0.3, 0.8, ...]  (1536维)
"假期" → [0.1, -0.3, 0.7, ...]  (距离很近,语义相似)
"工资" → [-0.5, 0.2, 0.1, ...]  (距离远,语义不同)

步骤 4:构建 RAG Chain

from langchain_core.prompts import ChatPromptTemplate
from langchain_community.chat_models import ChatTongyi
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough

# 准备组件
llm = ChatTongyi(model="qwen-turbo", temperature=0)

# 检索器(从向量库中检索相关文档)
retriever = vectorstore.as_retriever(
    search_type="similarity",  # 相似度搜索
    search_kwargs={"k": 3}     # 返回最相关的3个片段
)

# RAG 提示词模板
template = """基于以下参考资料回答问题:

参考资料:
{context}

问题:{question}

请根据参考资料回答,如果资料中没有相关信息,请说"根据现有资料无法回答"。
"""

prompt = ChatPromptTemplate.from_template(template)

# 格式化检索结果的函数
def format_docs(docs):
    return "\n\n".join([f"[{i+1}] {doc.page_content}" for i, doc in enumerate(docs)])

# 构建 RAG Chain
rag_chain = (
    {
        "context": retriever | format_docs,  # 检索并格式化文档
        "question": RunnablePassthrough()    # 直接传递用户问题
    }
    | prompt
    | llm
    | StrOutputParser()
)

# 使用
response = rag_chain.invoke("公司年假几天?")
print(response)

Chain 结构解析

用户问题
    ↓
{
    "context": retriever | format_docs,  →  检索相关文档并格式化
    "question": RunnablePassthrough()    →  原样传递问题
}
    ↓
prompt  →  填充模板
    ↓
llm     →  LLM 生成答案
    ↓
parser  →  解析输出
    ↓
最终答案

步骤 5:完整问答演示

# 测试问题
test_questions = [
    "公司年假几天?",
    "迟到怎么扣钱?",
    "团建预算多少?",
    "报销酒店标准是多少?",
    "公司创始人是谁?",  # 资料中没有,测试拒答能力
]

print("=" * 60)
print("RAG 问答演示")
print("=" * 60)

for question in test_questions:
    print(f"\n问题:{question}")
    
    # 显示检索到的相关片段
    docs = retriever.invoke(question)
    print("检索到的片段:")
    for i, doc in enumerate(docs):
        print(f"  {i+1}. {doc.page_content[:60]}...")
    
    # 生成答案
    answer = rag_chain.invoke(question)
    print(f"答案:{answer}")
    print("-" * 60)

完整练习代码

运行方式:

cd /Users/zhs/MyFiles/projects/test/test-agent/langchain-learn
source venv/bin/activate
python 06_rag.py

RAG 优化技巧

1. 调整检索数量

retriever = vectorstore.as_retriever(search_kwargs={"k": 5})  # 检索5个片段

2. 使用 MMR 多样化检索

retriever = vectorstore.as_retriever(
    search_type="mmr",           # 最大边际相关性
    search_kwargs={"k": 3, "fetch_k": 10}  # 从10个中选3个最 diverse 的
)

3. 添加过滤条件

retriever = vectorstore.as_retriever(
    search_kwargs={
        "k": 3,
        "filter": {"source": "company_policy.txt"}  # 只从特定文件检索
    }
)

练习任务

练习 1:多文档 RAG

加载多个文档(PDF、Word、TXT),构建统一的知识库。

练习 2:结合 Memory 的 RAG

让 RAG 系统记住用户之前的提问,支持追问(如"刚才说的年假,具体怎么申请?")。

练习 3:RAG + Agent

创建一个 Agent,能判断何时使用 RAG 检索,何时直接回答。


常见问题

Q1: 分割块大小怎么选?

建议

  • 问答场景:200-500 字符
  • 长文档摘要:1000-2000 字符
  • 代码:按函数/类分割

Q2: 检索不到相关内容怎么办?

  • 检查 Embedding 模型是否合适
  • 调整 chunk_sizeoverlap
  • 尝试不同的 search_type

Q3: 向量数据库可以持久化吗?

可以! Chroma 支持保存到磁盘,下次直接加载:

# 保存
vectorstore.persist()

# 加载
vectorstore = Chroma(
    persist_directory="./chroma_db",
    embedding_function=embeddings
)