本地大模型编程实战(11)与外部工具交互(2)
文章目录
- 准备
- 定义工具方法
- 创建提示词
- 生成工具方法实参
- 以 `json` 格式返回实参
- 自定义 `JsonOutputParser`
- 返回 `json`
- 调用工具方法
- 定义通用方法
- 用 链 返回结果
- 返回结果中包含工具输入
- 总结
- 代码
在使用 LLM(大语言模型)
时,经常需要调用一些自定义的工具方法完成特定的任务,比如:执行一些特殊算法、查询天气预报、旅游线路等。
很多大模型都具备使用这些工具方法的能力,Langchain
也为这些调用提供了便利。
之前的文章介绍了
llama3.1
与工具方法交互的实际例子,不过可惜langchain
对deepseek
支持还不够,导致:
- llm.bind_tools 根据用户问题生成的工具方法签名与
llama3.1
不同,在后续在调用工具方法时报错deepseek
返回的结果中包含了思考过程内容,显然Langchain
还不能正确解析出最终结果,这会导致langchain
的很多方法不能正常运行
这次我们将尝试通过以下两种方法解决 Langchain
使用 deepseek
时产生的上述问题:
- 使用提示词让大模型推理调用工具的方法名称和参数
- 使用自定义的
JsonOutputParser
处理deepseek
返回的信息
这里使用
llama3.1
和deepseek
等不同模型做对比,并不是为了说明孰优孰劣,而是仅仅为了技术演示需要。
准备
在正式开始撸代码之前,需要准备一下编程环境。
-
计算机
本文涉及的所有代码可以在没有显存的环境中执行。 我使用的机器配置为:- 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
等各种本地大模型。详见:
在langchian中使用本地部署的llama3.1大模型 。
定义工具方法
下面定义了两个简单的工具方法:计算加法和乘法:
def create_tools():
"""创建tools"""
@tool
def add(x: int, y: int) -> int:
"""计算a和b的和。"""
print (f"add is called...{x}+{y}")
return x + y
@tool
def multiply(x: int, y: int) -> int:
"""计算a和b的乘积。"""
print (f"multiply is called...{x}*{y}")
return x * y
tools = [add, multiply]
for t in tools:
print("--")
print(t.name)
print(t.description)
print(t.args)
return tools
tools = create_tools()
上述代码执行后,会打印出 tools 的 名称 、描述 和 形参 :
--
add
计算a和b的和。
{'x': {'title': 'X', 'type': 'integer'}, 'y': {'title': 'Y', 'type': 'integer'}}
--
multiply
计算a和b的乘积。
{'x': {'title': 'X', 'type': 'integer'}, 'y': {'title': 'Y', 'type': 'integer'}}
创建提示词
rendered_tools = render_text_description(tools)
print(rendered_tools)
system_prompt = f"""\
您是一名助理,有权使用以下工具集。
以下是每个工具的名称和说明:
{rendered_tools}
根据用户输入,返回要使用的工具的名称和输入。
以 JSON blob 形式返回您的响应,其中包含“name”和“arguments”键。
“arguments”应该是一个字典,其中的键对应于参数名称,值对应于请求的值。
"""
prompt = ChatPromptTemplate.from_messages(
[("system", system_prompt), ("user", "{input}")]
)
render_text_description 方法生成了 tools 的描述:
add(x: int, y: int) -> int - 计算a和b的和。
multiply(x: int, y: int) -> int - 计算a和b的乘积。
这些描述在后面添加到提示词中,LLM
应该能通过这个完整的提示词生成工具方法的 参数(即:实参)了,我们后面试试看。
生成工具方法实参
定义测试方法:
def too_call(model_name,query):
llm = ChatOllama(model=model_name,temperature=0.1,verbose=True)
chain = prompt | llm
message = chain.invoke({"input": query})
print(f'response: \n{message.content}')
lamma3.1
和 deepseek
返回的结果为:
lamma3.1
{
"name": "multiply",
"arguments": {
"x": 3,
"y": 12
}
}
deepseek-r1
<think>
好,我现在需要解决用户的问题:“3乘以12等于多少?” 用户希望我作为助理,使用提供的工具来计算。首先,我要理解用户的需求是什么。
...
最后,我需要将这些信息整合成一个JSON对象,并确保语法正确,避免任何错误导致返回失败。这样,当用户调用这个工具时,就能得到正确的结果了。
</think>
```json
{
"name": "multiply",
"arguments": {
"x": 3,
"y": 12
}
}
```
llama3.1
和 deepseek-r1
都正确生成了工具方法的实参,只是 deepseek
包含了 <think>...</think>
块,后面需要将其中的 json
部分提取出来。
以 json
格式返回实参
自定义 JsonOutputParser
一般来说,结构化的数据在 链 中才好处理, JsonOutputParser
用于将结果转换为 json
格式,我们先自定义一个类,它继承自 JsonOutputParser
并能处理 deepseek
的返回文本。
class ThinkJsonOutputParser(JsonOutputParser):
def parse_result(self, result: list[Generation], *, partial: bool = False) -> Any:
"""将 LLM 调用的结果解析为 JSON 对象。支持deepseek。
Args:
result: LLM 调用的结果。
partial: 是否解析 partial JSON 对象。
If True, 输出将是一个 JSON 对象,其中包含迄今为止已返回的所有键。
If False, 输出将是完整的 JSON 对象。
默认值为 False.
Returns:
解析后的 JSON 对象。
Raises:
OutputParserException: 如果输出不是有效的 JSON。
"""
text = result[0].text
text = text.strip()
# 判断是否为 deepseek生成的内容,如果是的话,提取其中的json字符串
if '<think>' in text and '</think>' in text:
match = re.search(r'\{.*\}', text.strip(), re.DOTALL)
if match:
text = match.group(0)
result[0].text = text
return super().parse_result(result, partial=partial)
上述方法使用正则表达式将 deepseek
返回的 json
内容提取出来,这样处理后,deepseek
就可以加入 langchain
中了。
返回 json
query = "3 * 12等于多少?"
def too_call_json(model_name,query):
"""以json格式输出"""
llm = ChatOllama(model=model_name,temperature=0.1,verbose=True)
chain = prompt | llm | ThinkJsonOutputParser()
message =chain.invoke({"input": query})
print(f'JsonOutputParser: \n{message}')
我们用两个大模型分别测试,这次返回的结果一样:
{'name': 'multiply', 'arguments': {'x': 3, 'y': 12}}
调用工具方法
定义通用方法
我们先定义一个通用的调用工具方法的方法:
class ToolCallRequest(TypedDict):
"""invoke_tool 函数使用的参数格式。"""
name: str
arguments: Dict[str, Any]
def invoke_tool(
tool_call_request: ToolCallRequest, config: Optional[RunnableConfig] = None
):
"""执行工具调用的函数。
Args:
tool_call_request: 包含键名和参数的字典。
`name` 必须与已存在的工具名称匹配。
`arguments` 是工具函数的参数。
config: 这是 LangChain 使用的配置信息,其中包含回调、元数据等内容。
Returns:
requested tool 的输出
"""
tool_name_to_tool = {tool.name: tool for tool in tools}
name = tool_call_request["name"]
requested_tool = tool_name_to_tool[name]
return requested_tool.invoke(tool_call_request["arguments"], config=config)
用 链 返回结果
现在我们可以用 langchain
整合以上成果:
def invoke_chain(model_name,query):
llm = ChatOllama(model=model_name,temperature=0.1,verbose=True)
chain = prompt | llm | ThinkJsonOutputParser() | invoke_tool
result =chain.invoke({"input": query})
print(f'invoke_chain:\n{result}')
调用此方法,我们会发现 llama3.1
和 deepseek-r1
都返回了简单的结果:
36
返回结果中包含工具输入
返回工具输出和工具输入都很有帮助。我们可以通过 RunnablePassthrough.assign 输出,这将获取 RunnablePassthrough 组件的输入并为其添加一个键,同时仍传递当前输入中的所有内容。
def invoke_chain_with_input(model_name,query):
llm = ChatOllama(model=model_name,temperature=0.1,verbose=True)
from langchain_core.runnables import RunnablePassthrough
chain = (
prompt | llm | ThinkJsonOutputParser() | RunnablePassthrough.assign(output=invoke_tool)
)
result = chain.invoke({"input": query})
print(f'invoke_chain with input:\n{result}')
调用此方法,我们会发现 llama3.1
和 deepseek-r1
返回了同样的结果:
{'name': 'multiply', 'arguments': {'x': 3, 'y': 12}, 'output': 36}
完美!
总结
在这篇文章里,我们通过直接使用提示词和自定义 json解析类
的方法,让 deepseek-r1
也完美的嵌入到 langchain
中,从而完成了对 工具方法 的调用。
代码
本文涉及的所有代码以及相关资源都已经共享,参见:
- github
- gitee
参考:
- How to add ad-hoc tool calling capability to LLMs and Chat Models
🪐祝好运🪐