创建和运行Agent
单独来说,大语言模型无法采取行动 - 它们只能输出文本。
LangChain 的一个重要用例是创建代理。
代理是使用 LLM 作为推理引擎的系统,用于确定应采取哪些行动以及这些行动的输入应该是什么。
然后可以将这些行动的结果反馈给代理,并确定是否需要更多行动,或者是否可以结束。
我们将构建一个可以与多种不同工具进行交互的代理:一个是本地数据库,另一个是搜索引擎。您将能够向该代理提问,观察它调用工具,并与它进行对话。
下面将介绍使用 LangChain 代理进行构建。LangChain 代理适合入门,但在一定程度之后,我们可能希望拥有它们无法提供的灵活性和控制性。要使用更高级的代理,我们建议查看LangGraph
概念
我们将涵盖的概念包括:
- 使用语言模型,特别是它们的工具调用能力
- 创建检索器以向我们的代理公开特定信息
- 使用搜索工具在线查找信息
- 聊天历史,允许聊天机器人 "记住" 过去的交互,并在回答后续问题时考虑它们
- 使用LangSmith调试和跟踪您的应用程序
定义工具
我们首先需要创建我们想要使用的工具。我们将使用两个工具:Tavily(用于在线搜索),然后是我们将创建的本地索引上的检索器。
Tavily
LangChain 中有一个内置工具,可以轻松使用 Tavily 搜索引擎作为工具。
请注意,这需要一个 API 密钥 - 他们有一个免费的层级,但如果您没有或不想创建一个,您可以忽略这一步。
创建 API 密钥后,您需要将其导出为:
export TAVILY_API_KEY="..."
python
import os
from langchain_community.tools.tavily_search import TavilySearchResults
def _clean_text(text: str, max_len: int = 16000) -> str:
one_line = " ".join(text.split())
if len(one_line) <= max_len:
return one_line
return one_line[:max_len] + "..."
search = TavilySearchResults(max_results=2)
results = search.invoke("今天天津天气怎么样?")
for idx, item in enumerate(results, start=1):
title = item.get("title", "")
url = item.get("url", "")
score = item.get("score", "")
content = _clean_text(item.get("content", ""))
print(f"[{idx}] {title}")
print(f" url: {url}")
print(f" score: {score}")
print(f" summary: {content}")
print("-" * 60)
Retriever
Retriever 是 langchain 库中的一个模块,用于检索工具。检索工具的主要用途是从大型文本集合或知识库中找到相关信息。它们通常用于问答系统、对话代理和其他需要从大量文本数据中提取信息的应用程序。
我们还将在自己的一些数据上创建一个Retriever。有关每个步骤的更深入解释,请参阅此教程。
安装阿里云百炼embeddingSDK
shell
pip install llama-index-embeddings-dashscope faiss-cpu opencc-python-reimplemented
百炼官网
https://bailian.console.aliyun.com/cn-beijing
配置APIKEY
export DASHSCOPE_API_KEY="..."
搭建网页知识库+语义检索的基础链路
python
import os
from langchain_community.document_loaders import WebBaseLoader
from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter
# 尽量在导入 loader 前设置,避免出现 USER_AGENT 警告
os.environ.setdefault("USER_AGENT", "my-langchain-retriever/1.0")
def _clean_text(text: str, max_len: int = 7000) -> str:
one_line = " ".join(text.split())
if len(one_line) <= max_len:
return one_line
return one_line[:max_len] + "..."
def _to_simplified_chinese(text: str) -> str:
"""尝试把繁体转为简体;未安装 opencc 时原样返回。"""
try:
from opencc import OpenCC # type: ignore
return OpenCC("t2s").convert(text)
except Exception:
return text
dashscope_api_key = os.getenv("DASHSCOPE_API_KEY")
if not dashscope_api_key:
raise ValueError(
"未检测到 DASHSCOPE_API_KEY。请先设置环境变量后再运行,例如:\n"
"PowerShell: $env:DASHSCOPE_API_KEY='你的key'"
)
# 定义网页加载器,强制使用 zh-cn 变体页面
loader = WebBaseLoader("https://zh.wikipedia.org/zh-cn/%E7%8C%AB")
# 抓取网页内容,返回Document列表(每个Document有page_content和metadata)
docks = loader.load()
# 把长文本切成小块(chunk)
documents = RecursiveCharacterTextSplitter(
chunk_size=1000, # 每块约1000字符
chunk_overlap=200, # 相邻块重叠200字符,减少语义断裂
).split_documents(docks)
# 配置embedding模型(阿里云百炼OpenAI兼容接口)
embeddings = OpenAIEmbeddings(
model="text-embedding-v4", # 向量模型名
openai_api_key=dashscope_api_key, # 你的百炼key
openai_api_base="https://dashscope.aliyuncs.com/compatible-mode/v1", # 百炼兼容接口地址
# 百炼兼容接口要求 input 为字符串或字符串列表,这里禁用 token 数组输入
check_embedding_ctx_length=False,
# 百炼单次批量上限为 10
chunk_size=10,
)
# 把每个文档块转向量,并写入FAILL向量索引(本地内存索引)
vector = FAISS.from_documents(documents, embeddings)
# 把向量库包装成检索器接口,方便query检索
retriever = vector.as_retriever()
# 用问题做语义检索,返回最相关文档快列表,取第一条作为最相关结果
doc = retriever.invoke("猫的特征是什么?")[0]
title = doc.metadata.get("title", "")
source = doc.metadata.get("source", "")
language = doc.metadata.get("language", "")
summary = _clean_text(doc.page_content)
summary = _to_simplified_chinese(summary)
print("=== Retriever Result ===")
print(f"Title : {title}")
print(f"Source : {source}")
print(f"Language: {language}")
print("Summary :")
print(summary)
现在,我们已经填充了我们将要进行Retriever的索引,我们可以轻松地将其转换为一个工具(代理程序正确使用所需的格式)。
python
from langchain_core.tools import StructuredTool
def search_cat_knowledge(query: str) -> str:
"""从猫的知识库中检索与问题最相关的内容摘要。"""
doc = retriever.invoke(query)[0]
title = doc.metadata.get("title", "")
source = doc.metadata.get("source", "")
language = doc.metadata.get("language", "")
summary = _to_simplified_chinese(_clean_text(doc.page_content, max_len=1000))
return (
f"Title: {title}\n"
f"Source: {source}\n"
f"Language: {language}\n"
f"Summary: {summary}"
)
# 这是"retriever 封装成 tool"的可用写法
retriever_tool = StructuredTool.from_function(
func=search_cat_knowledge,
name="cat_knowledge_retriever",
description="检索猫相关知识,输入一个问题字符串并返回摘要。",
return_direct=True,
)
print("\n=== Retriever Tool Result ===")
print(retriever_tool.invoke({"query": "猫的听觉有什么特点?"}))
工具
既然我们都创建好了,我们可以创建一个工具列表,以便在下游使用。
tools = [search, retriever_tool]
使用语言模型
接下来,让我们学习如何使用语言模型来调用工具。LangChain支持许多可以互换使用的不同语言模型 - 选择您想要使用的语言模型!
您可以传入消息列表来调用语言模型。默认情况下,响应是一个content字符串。
python
from langchain_deepseek import ChatDeepSeek
from langchain_core.messages import HumanMessage
model = ChatDeepSeek(model="deepseek-chat", temperature=0)
response = model.invoke([HumanMessage(content="hi!")])
print(response.content)
现在,我们可以看看如何使这个模型能够调用工具。为了使其具备这种能力,我们使用 .bind_tools 来让语言模型了解这些工具。
python
from langchain_deepseek import ChatDeepSeek
from langchain_core.messages import HumanMessage
from tools_tavily import search
from tools_retriever import retriever_tool
model = ChatDeepSeek(model="deepseek-chat", temperature=0)
tools = [search, retriever_tool]
model_with_tools = model.bind_tools(tools)
现在我们可以调用模型了。让我们首先用一个普通的消息来调用它,看看它的响应。我们可以查看content字段和tool_calls字段。
python
response = model_with_tools.invoke([HumanMessage(content="你好")])
print("ContentString: ", response.content)
print("ToolCalls:", response.tool_calls)
输出:
text
ContentString: 你好!很高兴见到你!😊
有什么我可以帮你的吗?无论是想了解猫咪的知识、查询最新信息,还是随便聊聊天,我都很乐意陪你!
ToolCalls: []
现在,让我们尝试使用一些期望调用工具的输入来调用它。
python
response = model_with_tools.invoke([HumanMessage(content="今天天津天气怎么样?")])
print("ContentString: ", response.content)
print("ToolCalls:", response.tool_calls)
输出:
text
ContentString: 我来帮你查一下今天天津的天气情况。
ToolCalls: [{'name': 'tavily_search_results_json', 'args': {'query': '天津 今天 天气 2025年'}, 'id': 'call_00_ZpYVSf5DPTDFHfiaPx7y4864', 'type': 'tool_call'}]
我们可以看到现在没有内容,但有一个工具调用!它要求我们调用Tavily Search工具。
这并不是在调用该工具 - 它只是告诉我们要调用。为了实际调用它,我们将创建我们的代理程序。
创建代理程序
既然我们已经定义了工具和LLM,我们可以创建代理程序。我们将使用一个工具调用代理程序。
现在,我们可以使用LLM、提示和工具初始化代理。代理负责接收输入并决定采取什么行动。关键的是,代理不执行这些操作 - 这是由AgentExecutor(下一步)完成的。
python
from langchain_deepseek import ChatDeepSeek
from langchain_core.messages import AIMessage, HumanMessage
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_classic.agents import AgentExecutor, create_tool_calling_agent
from tools_tavily import search
from tools_retriever import retriever_tool
model = ChatDeepSeek(model="deepseek-chat", temperature=0)
tools = [search, retriever_tool]
prompt = ChatPromptTemplate.from_messages(
[
("system", "你是一个有帮助的助手,请优先使用工具回答事实类问题。"),
MessagesPlaceholder(variable_name="chat_history"),
("human", "{input}"),
MessagesPlaceholder(variable_name="agent_scratchpad"), # agent执行过程中需要的指令
]
)
# tools: 给Agent规划层用的,让大模型知道有哪些工具可以调用,每个工具的参数是什么
# 从而决定要不要调用工具,调用哪个工具
# 声明可选工具
agent = create_tool_calling_agent(model, tools, prompt)
# tools: 给Agent执行层用户,当Agent产出tool call后
# Executor需要拿到真实的工具对象去执行,并管理中间步骤
# 注册可执行工具
# verbose=True: 打印Agent的运行过程,进入/结束chain、模型的中间推理步骤、调用了哪个工具/工具入参是什么、工具返回了什么
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
chat_history = [
HumanMessage(content="你好,我想了解猫。"),
AIMessage(content="好的,请问你想了解猫的哪些方面?"),
]
result = agent_executor.invoke({"input": "猫的特征是什么?今天天津天气怎么样?", "chat_history": chat_history})
print(result["output"])
大模型来分析调用哪个工具,真正调用工具的执行者是Agent,Agent会根据输入将Task任务做拆分。
添加记忆
如前所述,此代理是无状态的。这意味着它不会记住先前的交互。要给它记忆,我们需要传递先前的 chat_history。注意:由于我们使用的提示,它需要被称为chat_history。如果我们使用不同的提示,我们可以更改变量名
python
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
chat_history = [
HumanMessage(content="你好,我的名字是Jack。"),
AIMessage(content="好的,Jack你好。"),
]
result = agent_executor.invoke({"input": "我叫啥?", "chat_history": chat_history})
print(result["output"])
如果我们想要自动跟踪这些消息,我们可以将其包装在一个RunnableWithMessageHistory中
python
from langchain_deepseek import ChatDeepSeek
from langchain_core.messages import AIMessage, HumanMessage
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_classic.agents import AgentExecutor, create_tool_calling_agent
from langchain_core.chat_history import BaseChatMessageHistory
from langchain_community.chat_message_histories import ChatMessageHistory
from tools_tavily import search
from tools_retriever import retriever_tool
store = {}
def get_session_history(session_id: str) -> BaseChatMessageHistory:
if session_id not in store:
store[session_id] = ChatMessageHistory()
return store[session_id]
model = ChatDeepSeek(model="deepseek-chat", temperature=0)
tools = [search, retriever_tool]
prompt = ChatPromptTemplate.from_messages(
[
("system", "你是一个有帮助的助手,请优先使用工具回答事实类问题。"),
MessagesPlaceholder(variable_name="chat_history"),
("human", "{input}"),
MessagesPlaceholder(variable_name="agent_scratchpad"), # agent执行过程中需要的指令
]
)
# tools: 给Agent规划层用的,让大模型知道有哪些工具可以调用,每个工具的参数是什么
# 从而决定要不要调用工具,调用哪个工具
# 声明可选工具
agent = create_tool_calling_agent(model, tools, prompt)
# tools: 给Agent执行层用户,当Agent产出tool call后
# Executor需要拿到真实的工具对象去执行,并管理中间步骤
# 注册可执行工具
# verbose=True: 打印Agent的运行过程,进入/结束chain、模型的中间推理步骤、调用了哪个工具/工具入参是什么、工具返回了什么
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
def invoke_with_session(session_id: str, user_input: str) -> str:
history = get_session_history(session_id)
response = agent_executor.invoke(
{
"input": user_input,
"chat_history": history.messages,
}
)
output = response["output"]
history.add_user_message(user_input)
history.add_ai_message(output)
return output
response = invoke_with_session("123", "Hi,我的名字是Maya")
print(response)
response = invoke_with_session("123", "我叫啥?")
print(response)
Agent的核心是由几个组成驱动的:
- 工具:大模型匹配对应的工具、Agent负责调用
- 提示词:先做任务规划、然后拆分成多个子任务