在上一篇文章中,我们实践了如何使用 langchain
创建一个简单的 ReAct Agent(智能体)
,这次我们升级一下玩法:实现一个可以调用两个 工具 的智能体。
其中一个工具用户查询天气预报,另外一个工具用来查询知识库,实际上该工具实现了 RAG(Retrieval Augmented Generation,检索增强生成)
。
此次我们使用
qwen2.5
,llama3.1
和deepseek
。感觉在处理中文方面,感觉qwen2.5
比llama3.1
要好一些。
准备
在正式开始撸代码之前,需要准备一下编程环境。
-
计算机
本文涉及的所有代码可以在没有显存的环境中执行。 我使用的机器配置为:
- CPU: Intel i5-8400 2.80GHz
- 内存: 16GB
-
Visual Studio Code 和 venv 这是很受欢迎的开发工具,相关文章的代码可以在
Visual Studio Code
中开发和调试。 我们用python
的venv
创建虚拟环境, 详见:
在Visual Studio Code中配置venv。 -
Ollama 在
Ollama
平台上部署本地大模型非常方便,基于此平台,我们可以让langchain
使用llama3.1
、qwen2.5
、deepseek
等各种本地大模型。详见:
在langchian中使用本地部署的llama3.1大模型 。
用于查天气的工具
我们构建一个简单的工具,它用于查询某个城市的天气情况:
python
@tool(parse_docstring=True)
def get_wheather_info(
city_name: str = '' #不设置默认值可能导致LLM强行解析city_name出错或者强行调用这个tool
) -> str:
"""获取某个城市的天气信息。
Args:
city_name: 城市名称。
"""
print(f'Getting weather information for:{city_name}')
if not city_name:
return "缺少 city_name 参数,无法检索天气信息。"
"""
**这个返回很重要**
返回错误后,agent会放弃这个结果,用自己的能力回答问题,这样结果更加合理;
否则,agent会使用空的city_name调用这个tool,并且会拼凑出new york的天气或者别的天气方面的信息。
"""
else:
return f"{city_name}的气温是25摄氏度。"
通过设定参数默认值、在代码中对参数做有效性校验,并在参数有问题返回明确的信息,可以有效的防止
LLM(大语言模型)
乱用工具函数。
我们通过下面的方法测试一下:
python
def test_get_wheather_info(llm_model_name,city_name):
"""测试获取天气信息"""
print(f'--------{llm_model_name}----------')
tools = [
get_wheather_info,
]
llm = ChatOllama(model=llm_model_name,temperature=0,verbose=True)
llm_with_tools = llm.bind_tools(tools)
query = f'{city_name}的天气怎么样?'
ai_msg = llm_with_tools.invoke(query)
print(f'get_wheather_info tool_calls:\n{ai_msg.tool_calls}')
由于
langchain
的 bind_tools 函数对deepseek-r1
支持不好,执行时出错,所以我们使用MFDoom/deepseek-r1-tool-calling:7b
。
qwen2.5
和 llama3.1
生成的 tool_calls 一样:
json
[
{'name': 'get_wheather_info', 'args': {'city_name': '北京'}, 'id': 'bf852b33-2e36-4cf9-a9ff-5eb8bb3b7ae3', 'type': 'tool_call'}
]
MFDoom/deepseek-r1-tool-calling:7b
生成的 tool_calls 比较复杂一些:
json
[
{'name': 'function name', 'args': {'param1': 'value1'}, 'id': 'e7698bda-c759-4d8f-903b-239447c9b2c1', 'type': 'tool_call'},
{'name': 'get_wheather_info', 'args': {'city_name': 'Beijing'}, 'id': '240a07a7-1a65-4bbc-9851-92d86c9edbe8', 'type': 'tool_call'},
{'name': 'get_wheather_info', 'args': {'city_name': 'Beijing'}, 'id': '735b4aee-b58e-4d38-8c82-980656f65c77', 'type': 'tool_call'}
]
不知为什么要把
北京
转换成Beijing
。
建立矢量数据库
我们使用网页做内容源,拆分并矢量化后,存储在本地。 下面先定义处理矢量数据库的类:
python
class LocalVectorDBChroma:
"""使用Chroma在本地处理适量数据库"""
def __init__(self,model_name,persist_directory,delimiter = ","):
self._embedding = OllamaEmbeddings(model=model_name)
self._persist_directory = persist_directory
self._delimiter = delimiter
def get_vector_store(self):
return Chroma(persist_directory=self._persist_directory,embedding_function=self._embedding)
def embed_documents_in_batches(self,documents,batch_size=3):
"""
按批次嵌入,可以显示进度。
vectordb会自动持久化存储在磁盘。
"""
vectordb = Chroma(persist_directory=self._persist_directory,embedding_function=self._embedding)
for i in tqdm(range(0, len(documents), batch_size), desc="嵌入进度"):
batch = documents[i:i + batch_size]
# 从文本块生成嵌入,并将嵌入存储在本地磁盘。
vectordb.add_documents(batch)
def embed_webpage(self,url):
"""嵌入网页"""
from langchain_community.document_loaders import WebBaseLoader
loader = WebBaseLoader(url,encoding="utf-8") # 增加encoding参数防止中文乱码
docs = loader.load()
documents = RecursiveCharacterTextSplitter(
chunk_size=1000, chunk_overlap=200
).split_documents(docs)
self.embed_documents_in_batches(documents)
将文本转化成矢量的过程称之为嵌入 ,语义相近的文本转化的矢量之间的空间距离较短,所以在做矢量检索时,可以根据语义 而不是关键词来查找相近的结果。
网页的段落文字通常较多,所以这里设置的 chunk_size 也比较大。
执行嵌入:
python
from common.MyVectorDB import LocalVectorDBChroma
def create_db(model_name):
"""生成本地矢量数据库"""
persist_directory = get_persist_directory(model_name)
if os.path.exists(persist_directory):
return
db = LocalVectorDBChroma(model_name,persist_directory)
db.embed_webpage("http://wfcoding.com/articles/programmer/p0102/")
执行完毕上述代码后,本地硬盘上应该会多一个文件夹 es_shaw ,该文件夹中的内容就是矢量数据。
创建检索器(retriever)
下面我们可以基于已创建的矢量数据库,创建 检索器(retriever)
:
python
def create_retriever(embed_model_name):
"""创建检索器"""
persist_directory = get_persist_directory(embed_model_name)
db = LocalVectorDBChroma(embed_model_name,persist_directory)
# 基于Chroma 的 vector store 生成 检索器
vector_store = db.get_vector_store()
retriever = vector_store.as_retriever(
search_type="similarity",
search_kwargs={"k": 2},
)
return retriever
此检索器返回2篇文档,由于网页内容已经被切割,这样做可以避免只有一篇文档容易导致检索出来的内容不完整的问题。
准备工具集
下面我们用准备好的检索器生成一个工具:elastic_search,再组合另外一个查询天气的工具,创建工具集:
python
def create_tools(embed_model_name):
"""创建工具集"""
retriever_tool = create_retriever_tool(
create_retriever(embed_model_name),
"elastic_search",
"当您搜索有关 elasticsearch 的知识时才使用此工具!",
)
tools = [get_wheather_info, retriever_tool]
return tools
我们步步为营,先定义一个函数,来测试一下工具集是否能够正常生成 tool_calls,只有可以正常生成 tool_calls,智能体才可能正确的调用工具函数。
python
def test_tools(llm_model_name,embed_model_name,querys):
"""测试工具集"""
llm = ChatOllama(model=llm_model_name,temperature=0,verbose=True)
tools = create_tools(embed_model_name)
llm_with_tools = llm.bind_tools(tools)
print(f'--------{llm_model_name}----------')
for query in querys:
response = llm_with_tools.invoke([HumanMessage(content=query)])
print(f"ContentString:\n {response.content}")
print(f"ToolCalls: \n{response.tool_calls}")
我们用三个问题进行测试,看看三个大模型表现如何:
python
querys = ["你好,你擅长能做什么?","上海的天气怎么样?","如何实现elasticsearch的深度分页?"]
qwen2.5
text
ContentString:
您好!我可以帮助您查询天气信息、回答一些常识性问题或者提供关于Elasticsearch的知识。请告诉我具体需要什么帮助呢?
ToolCalls:
[]
ContentString:
ToolCalls:
[{'name': 'get_wheather_info', 'args': {'city_name': '上海'}, 'id': '3151df40-6b67-4683-8f68-2bd453d4cbed', 'type': 'tool_call'}]
ContentString:
ToolCalls:
[{'name': 'elastic_search', 'args': {'query': '如何实现elasticsearch的深度分页'}, 'id': '7612018c-c63b-4e03-8321-478240944a99', 'type': 'tool_call'}]
qwen2.5
做得很棒,非常聪明的处理了所有问题。甚至在回答第一个问题时,把工具的功能也融合进去了。
llama3.1
text
ContentString:
ToolCalls:
[{'name': 'get_wheather_info', 'args': {'city_name': ''}, 'id': 'c13eb0b9-8aa8-48fc-9d32-3511795a57d1', 'type': 'tool_call'}]
ContentString:
ToolCalls:
[{'name': 'get_wheather_info', 'args': {'city_name': '北京'}, 'id': 'c6ad345f-ecc3-494a-80a8-47a65b657bea', 'type': 'tool_call'}]
ContentString:
ToolCalls:
[{'name': 'elastic_search', 'args': {'query': 'elasticsearch 深度分页'}, 'id': 'd154d4f6-d129-4298-a36f-25d5b957538d', 'type': 'tool_call'}]
llama3.1
在处理第1个问题时,生成了一个无效的 tool_calls ; 在处理第2个问题时,把 city_name 错误的推理成了北京
,只有第1个问题推理得准确无误。
MFDoom/deepseek-r1-tool-calling:7b
text
ContentString:
ToolCalls:
[{'name': 'get_wheather_info', 'args': {'city_name': '上海'}, 'id': '4db2f88b-b76a-4d39-8673-8fea228f375c', 'type': 'tool_call'}]
ContentString:
ToolCalls:
[{'name': 'get_wheather_info', 'args': {'city_name': '上海'}, 'id': 'c5c7b229-f99d-4057-8c21-af679ffba827', 'type': 'tool_call'}]
ContentString:
ToolCalls:
[{'name': 'get_wheather_info', 'args': {'city_name': '上海'}, 'id': '303273d4-5edb-47ed-b2ab-8f26200df6cd', 'type': 'tool_call'}]
MFDoom/deepseek-r1-tool-calling:7b
也只有第2个问题推理正确。
创建智能体
工具集创建完毕了,现在该给它们装上大脑,即智能体了。
我们先用 create_tool_calling_agent
方法创建 智能体对象:
python
def create_agent(llm_model_name,embed_model_name):
"""创建智能体"""
from langchain_core.tools import render_text_description
tools = create_tools(embed_model_name)
# 此prompt是基于hwchase17/openai-functions-agent修改的
systemprompt = """\
您是一名助理,有权使用以下工具集。
下面是每个工具的名称和说明:
[get_wheather_info, elastic_search]
- **仅在需要时使用上述工具集!**
- 如果没有可靠的依据来确定 city_name,则不要调用 get_wheather_info!
"""
prompt = ChatPromptTemplate([
("system", systemprompt),
("placeholder", "{chat_history}"),
("human", "{input}"),
("placeholder", "{agent_scratchpad}"),
])
llm = ChatOllama(model=llm_model_name,temperature=0,verbose=True)
agent = create_tool_calling_agent(llm, tools, prompt)
return agent
再创建 一个 AgentExecutor
,创建完毕后,就可以执行这个智能体了:
python
def create_agent_executor(llm_model_name,embed_model_name):
"""创建agent_executor"""
tools = create_tools(embed_model_name)
agent = create_agent(llm_model_name,embed_model_name)
agent_executor = AgentExecutor(agent=agent, tools=tools)
"""实际上在create_agent中已经创建了tools,这里还要再传入tools,似乎有点多余。"""
return agent_executor
测试智能体
下面我们定义测试方法,用不同的大模型执行这个智能体:
python
def test_agent_executor(llm_model_name,embed_model_name,querys):
"""测试AgentExecutor"""
print(f'--------{llm_model_name}----------')
agent_executor = create_agent_executor(llm_model_name,embed_model_name)
for query in querys:
r = agent_executor.invoke({"input": query})
print(f'agent_executor.invoke:\n{r}')
下面就是见证奇迹的时刻了,我们还是用同样的3个问题,用不同的大模型驱动智能体。
python
querys = ["你好,你擅长能做什么?","上海的天气怎么样?","如何实现elasticsearch的深度分页?"]
qwen2.5
json
{'input': '你好,你擅长能做什么?', 'output': '我可以帮助您查询天气信息、或者在特定情况下搜索相关信息。请告诉我您需要什么具体帮助呢?例如,您可以询问某个城市的天气情况,或者有关 Elasticsearch 的问题。'}
{'input': '上海的天气怎么样?', 'output': '上海现在的温度是25摄氏度。'}
{'input': '如何实现elasticsearch的深度分页?', 'output': '在 Elasticsearch 中,使用 `search_after` 实现深度分页是一种有效的方法。...
使用 `search_after` 可以有效地处理大量数据的分页问题。这种方法避免了因结果窗口过大而引发的问题,并且可以灵活地进行深度分页操作。\n\n希望这些信息对你有所帮助!如果你有任何其他问题,请随时提问。'}
qwen2.5
表现稳定,第2、3个问题显然是基于调用工具获取的信息,回答得很好!
llama3.1
json
{'input': '你好,你擅长能做什么?', 'output': '我可以使用工具集来帮助回答问题!例如,如果你问"今天是几月几号",我可以使用 [get_wheather_info, elastic_search] 来获取当前日期的信息。如果你提供城市名称,我也可以 使用 get_wheather_info 来获取天气预报。'}
{'input': '上海的天气怎么样?', 'output': 'Sorry, 我没有找到关于上海的天气信息。您可以尝试提供更多信息或使用不同的工具来获取答案。'}
{'input': '如何实现elasticsearch的深度分页?', 'output': '答案:使用 search_after 进行深度分页可以避免 elasticsearch 中的 Result window is too large 错误,并且可以提高查询效率和减少内存占用。首先,确定一个可以唯一确定一条文档的键,然后在第一次查询中指定排序规则。在后续查询中,将第一次查询结果的最后一条记录的 sort 值作为 search_after 的参数,可以实现深度分页。'}
果然,llama3.1
在回答第2、3个问题时,并没有正确的调用工具。
MFDoom/deepseek-r1-tool-calling:7b
json
{'input': '你好,你擅长能做什么?', 'output': '<think>\n嗯,用户问:"你好,你擅长能做什么?" 这是一个常见的问候,我需要回应得友好且明确。首先,我要确认自己能够帮助用户解决什么类型的问题。\n\n根据提供的工具集,有两个可用的工具:get_wheather_info 和 elastic_search。get_wheather_info 用于获取某个城市的天气信息,但需要 city_name 参数,...。\n</think>\n\n您好!我目前 的功能主要是帮助您回答问题、提供信息和解答疑问等。如果您有具体的问题或需要帮助的地方,请随时告诉我,我会尽力为您提供详细的解答和帮助。\n\n如果需要调用某个工具(如 `get_wheather_info` 或 `elastic_search`),请 告诉我具体需求,并提供相关的参数,我会根据您的要求调用相应的工具来为您服务!'}
{'input': '上海的天气怎么样?', 'output': 'Agent stopped due to max iterations.'}
{'input': '如何实现elasticsearch的深度分页?', 'output': '</think>\n\n要实现Elasticsearch的深度分页,可以按照以下步骤操作:\n\n1. **定义搜索查询**:使用`get_wheather_info`获取所需城市的天气信息,并构建一个包含城市名称、温度等字段的查询。\n\n2. **设置分页参数**:\n ... \n\n通过以上步骤,可以实现对Elasticsearch数据的深度分页功能。'}
可能因为提示词或者与 langchain
结合的不好,它也没有正确的调用工具。
总结
通过上述编码演练,我们发现使用 qwen2.5
作为 LLM(大语言模型)
处理中文比较稳定,驱动智能体也游刃有余;使用 shaw/dmeta-embedding-zh
做中文嵌入检索效果也不错,值得在 RAG(Retrieval Augmented Generation,检索增强生成)
场景中使用。
到现在,我们还不太清楚这个 Agent(智能体)
的思考过程,也并未填充提示词中的 {chat_history} 集成消息历史,我们在下一篇文章中再进一步完善。
代码
本文涉及的所有代码以及相关资源都已经共享,参见:
为便于找到代码,程序文件名称最前面的编号与本系列文章的文档编号相同。
🪐感谢您观看,祝好运🪐