欢迎来到啾啾的博客🐱。
记录学习点滴。分享工作思考和实用技巧,偶尔也分享一些杂谈💬。
有很多很多不足的地方,欢迎评论交流,感谢您的阅读和评论😄。
目录
1 引言
本篇,我们将使用LangChain来搭建Agent,LangChain为我们处理了ReAct循环、工具调用、提示词模板化和输出解析等大量繁琐的底层工作,让我们能专注于核心逻辑。
本篇将演示网络搜索与网页抓取工具,将LLM(DeepSeek)、搜索工具和特制的提示词组装成一个可执行的Agent。给它一个研究任务,观察它的“思考-行动”过程。
tools是Agent的力量源泉。ReAct是Agent的灵魂。
2 环境准备
- duckduckgo-search:一个免费易用的搜索引擎库
其他的都是老熟人了(等下就是)。
pip install langchain langchain-community duckduckgo-search beautifulsoup4
3 定义我们的工具箱 (Toolbox)
- LangChain的@tool注解
@tool 装饰器用于将 Python 函数标记为一个“工具”,供 LLM(如 GPT、Grok)在需要外部功能时调用。工具可以是任何函数,例如搜索、计算、API 调用等。
它会自动从函数的文档字符串 (docstring) 中提取出对工具的描述。
所以,我们必须清晰地告诉Agent “什么时候使用这个工具”和“输入应该是什么格式”。这是引导Agent正确行为的关键!
example:
定义工具
@tool
def search_tool(query: str) -> str:
"""
当需要在线搜索最新信息、查找文章链接或回答关于时事的问题时,使用此工具。
输入应该是一个精确的搜索查询语句。
例如:'LangChain Agents'
"""
print(f"\n>> 调用搜索工具,查询:'{query}'")
# DuckDuckGoSearchRun 是 LangChain 内置的一个简单搜索工具
from langchain_community.tools import DuckDuckGoSearchRun
search = DuckDuckGoSearchRun()
return search.run(query)
@tool
def scrape_website_tool(url: str) -> str:
"""
当需要从一个给定的网页URL中获取详细文本内容时,使用此工具。
输入必须是一个有效的URL。
例如:'https://www.promptingguide.ai/zh/techniques/react'
"""
print(f"\n>> 调用提取工具,提取 URL:'{url}' 上的文本")
try:
response = requests.get(url)
response.raise_for_status() # 检查请求是否成功
soup = BeautifulSoup(response.text, 'html.parser')
# 移除脚本和样式元素
for script_or_style in soup(["script", "style"]):
script_or_style.decompose()
# 获取文本并进行基本清理
text = soup.get_text()
lines = (line.strip() for line in text.splitlines())
chunks = (phrase.strip() for line in lines for phrase in line.split(" "))
text = '\n'.join(chunk for chunk in chunks if chunk)
# 为了避免内容过长,我们只返回前4000个字符
return text[:4000]
except requests.RequestException as e:
print(f"请求URL {url} 时出错: {e}")
return f"{url}抓取失败: {e}"
# 定义工具列表
tools = [
search_tool,
scrape_website_tool
]
4 创建并执行Agent
我们需要使用LangChain的Agent Executor来链接LLM和工具箱tools。
并且在构建AgentExecutor示例对象的时候,设置verbose=True,以打印出Agent完整的思考链,便于我们观察和调试。
完整example
import os
from dotenv import load_dotenv
from langchain.tools import tool
import requests
from bs4 import BeautifulSoup
from langchain_deepseek import ChatDeepSeek
from langchain import hub
from langchain.agents import create_react_agent, AgentExecutor
# 1. 加载环境变量
# 这会从 .env 文件中加载 DEEPSEEK_API_KEY
load_dotenv()
# 检查API密钥是否已设置
def get_deepseek_key():
key = os.getenv('DEEPSEEK_API_KEY')
if key is None:
raise ValueError("DEEPSEEK_API_KEY not found in environment variables.")
return key
# 2.定义工具
@tool
def search_tool(query: str) -> str:
"""
当需要在线搜索最新信息、查找文章链接或回答关于时事的问题时,使用此工具。
输入应该是一个精确的搜索查询语句。
例如:'LangChain Agents'
"""
print(f"\n>> 调用搜索工具,查询:'{query}'")
# DuckDuckGoSearchRun 是 LangChain 内置的一个简单搜索工具
from langchain_community.tools import DuckDuckGoSearchRun
search = DuckDuckGoSearchRun()
return search.run(query)
@tool
def scrape_website_tool(url: str) -> str:
"""
当需要从一个给定的网页URL中获取详细文本内容时,使用此工具。
输入必须是一个有效的URL。
例如:'https://www.promptingguide.ai/zh/techniques/react'
"""
print(f"\n>> 调用提取工具,提取 URL:'{url}' 上的文本")
try:
response = requests.get(url)
response.raise_for_status() # 检查请求是否成功
soup = BeautifulSoup(response.text, 'html.parser')
# 移除脚本和样式元素
for script_or_style in soup(["script", "style"]):
script_or_style.decompose()
# 获取文本并进行基本清理
text = soup.get_text()
lines = (line.strip() for line in text.splitlines())
chunks = (phrase.strip() for line in lines for phrase in line.split(" "))
text = '\n'.join(chunk for chunk in chunks if chunk)
# 为了避免内容过长,我们只返回前4000个字符
return text[:4000]
except requests.RequestException as e:
print(f"请求URL {url} 时出错: {e}")
return f"{url}抓取失败: {e}"
# 定义工具列表
tools = [
search_tool,
scrape_website_tool
]
# 3.LLM 初始化
def create_deepseek_llm():
api_key = get_deepseek_key()
if not api_key:
raise ValueError("没有ds key")
return ChatDeepSeek(
model = "deepseek-chat",
# temperature=0 表示我们希望Agent的思考过程尽可能稳定和可复现
temperature=0,
max_tokens=None,
timeout=None,
max_retries=2,
api_key=api_key
)
# 4.获取ReAct提示词模板
# LangChain Hub是一个存放高质量提示词模板的社区
# "hwchase17/react" 是ReAct框架的官方标准提示词
prompt = hub.pull("hwchase17/react")
print("--- 打印ReAct提示词模板,检查格式 ---")
print(prompt)
# 5.创建Agent
agent = create_react_agent(create_deepseek_llm(), tools, prompt)
# 6.创建Agent执行器
# Agent Executor负责实际运行ReAct循环,调用工具并获取观察结果
# verbose=True 会打印出Agent完整的思考链,非常便于我们观察和调试
agent_executor = AgentExecutor(agent=agent,tools = tools,verbose = True)
if __name__ == "__main__":
print("\n--- 开始执行Agent ---")
# 提出我们的研究问题
question = "对比Java和Python在构建LLM应用中的优劣势,并列出各自的主流框架。"
# 使用.invoke()方法来运行Agent
response = agent_executor.invoke({"input": question})
# 打印最终的输出结果
print("\n--- Agent执行完毕 ---")
print("最终答案:")
print(response['output'])
这段代码可以执行并返回有完整的ReAct循环:Thought—>Action—>Observation—>Thought。
同时,也展示了一个入门级问题——“LLM的行为与Agent执行器的期望不匹配”。
ValueError: An output parsing error occurred. In order to pass this error back to the agent and have it try again, pass `handle_parsing_errors=True` to the AgentExecutor. This is the error: Could not parse LLM output:
5 Agent注意事项
5.1 ReAct模板
ReAct提示词模板和普通模板存在结构上的差别,一般Prompt结构可以划分为六要素。
ReAct提示词模板算是特殊模板,强化了“规范思考过程和输出格式”,是一种“教学式”或“引导式”的模板。
它不要求LLM直接给答案,而是通过提供示例(Thought, Action, Observation的例子),来教会LLM一种思考和行动的“格式”或“流程”。它的目标是让LLM学会如何分解任务、使用工具,并以特定的结构化方式输出它的下一步计划。
在example中,我们的ReAct模板是:
input_variables=['agent_scratchpad', 'input', 'tool_names', 'tools'] input_types={} partial_variables={} metadata={'lc_hub_owner': 'hwchase17', 'lc_hub_repo': 'react', 'lc_hub_commit_hash': 'd15fe3c426f1c4b3f37c9198853e4a86e20c425ca7f4752ec0c9b0e97ca7ea4d'} template='Answer the following questions as best you can. You have access to the following tools:\n\n{tools}\n\nUse the following format:\n\nQuestion: the input question you must answer\nThought: you should always think about what to do\nAction: the action to take, should be one of [{tool_names}]\nAction Input: the input to the action\nObservation: the result of the action\n... (this Thought/Action/Action Input/Observation can repeat N times)\nThought: I now know the final answer\nFinal Answer: the final answer to the original input question\n\nBegin!\n\nQuestion: {input}\nThought:{agent_scratchpad}'
翻译一下是:
输入变量 = ['agent_scratchpad', 'input', 'tool_names', 'tools']
输入类型 = {}
部分变量 = {}
元数据 = {'lc_hub_owner': 'hwchase17', 'lc_hub_repo': 'react', 'lc_hub_commit_hash': 'd15fe3c426f1c4b3f37c9198853e4a86e20c425ca7f4752ec0c9b0e97ca7ea4d'}
模板 = ' 尽可能最佳地回答以下问题。你可以使用以下工具:\n\n {tools}\n\n 使用以下格式:\n\n 问题:你必须回答的输入问题 \n 思考:你应该始终思考该做什么 \n 动作:要采取的动作,必须是 [{tool_names}] 中的一个 \n 动作输入:动作的输入 \n 观察:动作的结果 \n...(这个思考 / 动作 / 动作输入 / 观察可以重复 N 次)\n 思考:我现在知道最终答案了 \n 最终答案:原始输入问题的最终答案 \n\n 开始!\n\n 问题:{input}\n 思考:{agent_scratchpad}'
模板的核心=引导示例+严格输出格式限制
- 核心示例:
- 看,这是第一个例子: 当问题是A时,你应该先Thought: …,然后Action: …,然后你会得到Observation: …,接着你再Thought: …,直到最后给出Final Answer: …。
- 看,这是第二个例子: 当问题是B时,你的思考和行动流程是这样的…
- 输出格式: 极其严格! 你必须严格按照Thought: …和Action: …的格式来输出你的下一步计划,或者用Final Answer: …来输出最终答案。绝对不能一次性输出所有内容。
5.2 LLM的行为与Agent执行器的期望不匹配
在example中,我们期望的是Agent中的LLM每一次只返回一个步骤(一个Thought或者Observation)。而我们的LLM一次返回了所有内容,包含了所有的思考、行动、观察、最终答案。
这样导致了一个后果,AgentExecutor的输出解析器 (Output Parser),无法解析DeepSeek返回的那个“过于完整”的响应。
可以通过以下方式进行调整
5.2.1 让Agent学会处理解析错误
我们可以告诉AgentExecutor:“如果你遇到了解析错误,不要直接崩溃。把这个错误信息包装一下,作为一次‘观察结果’反馈给LLM,让它自己意识到格式错了,然后重新生成一次。”
这需要在创建AgentExecutor时,加入一个参数 handle_parsing_errors=True。
# 修改前
# agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
# 修改后
agent_executor = AgentExecutor(
agent=agent,
tools=tools,
verbose=True,
handle_parsing_errors=True # <--- 添加这个参数
)
5.2.2 在提示词中明确约束
我们可以在提示词中更强硬地命令LLM,让它一次只输出一个步骤。但这需要修改从hub拉取下来的prompt对象,相对复杂一些,我们可以在掌握方案一之后再尝试。
在我们的example中,使用handle_parsing_errors=True
处理了错误,但是会导致LLM思考的上下文被污染,导致上下文出现Final Answer(理论上应该只出现一次),进而导致LLM思考异常,多次输出Final Answer。
因此,我们要修改我们的ReAct模板。
LangChain的默认hwchase17/react提示词模板对于某些模型来说,约束力可能不够强。
这里不得不提一句,DeepSeek有很强的“一步到位”倾向,这个ReAct模板调的头疼。
使用高度定制化、强约束、本地化的ReAct提示词模板。
- 明确的禁令 (Explicit Prohibitions): 直接用加粗、大写等方式,告诉模型“禁止”做什么。
- 单一任务原则 (Single Task Principle): 强调每一步只做一件事。
- 格式的绝对权威 (Format Absoluteness): 反复重申必须、严格、只能遵循给定的格式。
同时,要注意中文与英文解析不同,提示词模板(Prompt Template)和输出解析器(Output Parser)要适配。
且不同ReAct解析器能解析的单词也不一样,比如对于默认的ReAct解析器,我想使用下列格式就不行:
Thought: [一些思考]
Action:
```json
{
"action": "search_tool",
"action_input": "..."
}
其无法解析action与action_input,报错
Invalid Format: Missing 'Action Input:' after 'Action:'Thought
最终ReAct模板如下:
template = """
**指令:**
你是一个能够调用工具的智能助手。请严格按照以下格式,一步一步地回答问题。
**绝对禁止** 一次性输出所有步骤和最终答案。你**必须**一次只输出一个“思考”或“最终答案”区块。
**可用工具列表:**
{tools}
**输出格式要求:**
你的输出**必须严格**遵循以下两种格式之一:
**格式一:当你需要使用工具时**
Thought:[这里是你的思考过程,分析你需要做什么,决定使用哪个工具]
Action: [这里是你要调用的工具名称,必须是 {tool_names} 中的一个]
Action Input: [这里是传递给工具的输入参数]
**格式二:当你已经知道最终答案时,必须只包含'Final Answer'**
Thought:[这里是你总结性的思考,确认你已拥有足够信息来回答问题]
Final Answer:[这里是你对原始问题的最终回答]
**重要提醒:**
- 每一次返回,“Thought”+“Action”,要么是“Thought”+“Final Answer”。绝对不能两者都包含。
- “Final Answer”使用中文回答
**开始执行任务!**
**问题:** {input}
**思考过程日志:**
{agent_scratchpad}
"""
完整example如下,还增加了一个计数器,用于显示Thought轮次:
import os
from dotenv import load_dotenv
from langchain.tools import tool
import requests
from bs4 import BeautifulSoup
from typing import Any, Dict, List
from langchain_deepseek import ChatDeepSeek
from langchain import hub
from langchain.agents import create_react_agent, AgentExecutor
from langchain.callbacks.base import BaseCallbackHandler
from langchain.schema import AgentAction
from langchain.prompts import PromptTemplate
from langchain_community.tools import DuckDuckGoSearchRun
# 1. 加载环境变量
# 这会从 .env 文件中加载 DEEPSEEK_API_KEY
load_dotenv()
# 检查API密钥是否已设置
def get_deepseek_key():
key = os.getenv('DEEPSEEK_API_KEY')
if key is None:
raise ValueError("DEEPSEEK_API_KEY not found in environment variables.")
return key
# 2.定义工具
@tool
def search_tool(query: str) -> str:
"""
当需要在线搜索最新信息、查找文章链接或回答关于时事的问题时,使用此工具。
输入应该是一个精确的搜索查询语句。
例如:'LangChain Agents'
"""
print(f"\n>> 调用搜索工具,查询:'{query}'")
# DuckDuckGoSearchRun 是 LangChain 内置的一个简单搜索工具
search = DuckDuckGoSearchRun()
return search.run(query)
@tool
def scrape_website_tool(url: str) -> str:
"""
当需要从一个给定的网页URL中获取详细文本内容时,使用此工具。
输入必须是一个有效的URL。
例如:'https://www.promptingguide.ai/zh/techniques/react'
"""
print(f"\n>> 调用提取工具,提取 URL:'{url}' 上的文本")
try:
response = requests.get(url)
response.raise_for_status() # 检查请求是否成功
soup = BeautifulSoup(response.text, 'html.parser')
# 移除脚本和样式元素
for script_or_style in soup(["script", "style"]):
script_or_style.decompose()
# 获取文本并进行基本清理
text = soup.get_text()
lines = (line.strip() for line in text.splitlines())
chunks = (phrase.strip() for line in lines for phrase in line.split(" "))
text = '\n'.join(chunk for chunk in chunks if chunk)
# 为了避免内容过长,我们只返回前4000个字符
return text[:4000]
except requests.RequestException as e:
print(f"请求URL {url} 时出错: {e}")
return f"{url}抓取失败: {e}"
# 定义工具列表
tools = [
search_tool,
scrape_website_tool
]
# 3.LLM 初始化
def create_deepseek_llm():
api_key = get_deepseek_key()
if not api_key:
raise ValueError("没有ds key")
return ChatDeepSeek(
model = "deepseek-chat",
# temperature=0 表示我们希望Agent的思考过程尽可能稳定和可复现
temperature=0,
max_tokens=None,
timeout=None,
max_retries=2,
api_key=api_key
)
# ==============================================================================
# --- (新增) 自定义回调处理器,用于打印思考轮次 ---
# ==============================================================================
class IterationCounterCallback(BaseCallbackHandler):
"""一个在每次Agent行动前打印轮次的回调处理器"""
def __init__(self):
self.iteration_count = 0
def on_agent_action(
self,
action: AgentAction,
*,
run_id: Any,
parent_run_id: Any = None,
**kwargs: Any,
) -> Any:
"""在Agent执行一个行动时被调用。"""
# 我们将这个事件作为新一轮思考的开始
self.iteration_count += 1
print(f"\n--- 🤔 思考轮次: {self.iteration_count} ---")
# 4.获取ReAct提示词模板
# LangChain Hub是一个存放高质量提示词模板的社区
# "hwchase17/react" 是ReAct框架的官方标准提示词
# 我们不再从hub拉取,而是手动定义一个包含更强约束的模板
template = """
**指令:**
你是一个能够调用工具的智能助手。请严格按照以下格式,一步一步地回答问题。
**绝对禁止** 一次性输出所有步骤和最终答案。你**必须**一次只输出一个“思考”或“最终答案”区块。
**可用工具列表:**
{tools}
**输出格式要求:**
你的输出**必须严格**遵循以下两种格式之一:
**格式一:当你需要使用工具时**
Thought:[这里是你的思考过程,分析你需要做什么,决定使用哪个工具]
Action: [这里是你要调用的工具名称,必须是 {tool_names} 中的一个]
Action Input: [这里是传递给工具的输入参数]
**格式二:当你已经知道最终答案时,必须只包含'Final Answer'**
Thought:[这里是你总结性的思考,确认你已拥有足够信息来回答问题]
Final Answer:[这里是你对原始问题的最终回答]
**重要提醒:**
- 每一次返回,“Thought”+“Action”,要么是“Thought”+“Final Answer”。绝对不能两者都包含。
- “Final Answer”使用中文回答
**开始执行任务!**
**问题:** {input}
**思考过程日志:**
{agent_scratchpad}
"""
# 使用我们自定义的模板创建一个PromptTemplate对象
prompt = PromptTemplate.from_template(template)
# prompt = hub.pull("hwchase17/react")
print("--- 打印ReAct提示词模板,检查格式 ---")
print(prompt)
# 5.创建Agent
agent = create_react_agent(create_deepseek_llm(), tools, prompt)
# 6.创建Agent执行器
# Agent Executor负责实际运行ReAct循环,调用工具并获取观察结果
# verbose=True 会打印出Agent完整的思考链,非常便于我们观察和调试
agent_executor = AgentExecutor(agent=agent,tools = tools,verbose = True,handle_parsing_errors=True)
if __name__ == "__main__":
print("\n--- 开始执行Agent ---")
# 提出我们的研究问题
question = "对比Java和Python在构建LLM应用中的优劣势,并列出各自的主流框架。"
# 创建我们的回调处理器实例
iteration_counter = IterationCounterCallback()
# 使用.invoke()方法来运行Agent,通过config传入回调处理器
response = agent_executor.invoke(
{"input": question},
config={"callbacks": [iteration_counter]} # 挂载回调处理器
)
# 打印最终的输出结果
print("\n--- Agent执行完毕 ---")
print("最终答案:")
print(response['output'])