本地大模型编程实战(11)与外部工具交互(2)

devtools/2025/2/11 7:47:36/

文章目录

    • 准备
    • 定义工具方法
    • 创建提示词
    • 生成工具方法实参
    • 以 `json` 格式返回实参
      • 自定义 `JsonOutputParser`
      • 返回 `json`
    • 调用工具方法
      • 定义通用方法
      • 用 链 返回结果
      • 返回结果中包含工具输入
    • 总结
    • 代码


在使用 LLM(大语言模型) 时,经常需要调用一些自定义的工具方法完成特定的任务,比如:执行一些特殊算法、查询天气预报、旅游线路等。
很多大模型都具备使用这些工具方法的能力,Langchain 也为这些调用提供了便利。

之前的文章介绍了 llama3.1 与工具方法交互的实际例子,不过可惜 langchaindeepseek 支持还不够,导致:

  • llm.bind_tools 根据用户问题生成的工具方法签名与 llama3.1 不同,在后续在调用工具方法时报错
  • deepseek 返回的结果中包含了思考过程内容,显然 Langchain 还不能正确解析出最终结果,这会导致 langchain 的很多方法不能正常运行

这次我们将尝试通过以下两种方法解决 Langchain 使用 deepseek 时产生的上述问题:

  • 使用提示词让大模型推理调用工具的方法名称和参数
  • 使用自定义的 JsonOutputParser 处理 deepseek 返回的信息

这里使用 llama3.1deepseek 等不同模型做对比,并不是为了说明孰优孰劣,而是仅仅为了技术演示需要。

准备

在正式开始撸代码之前,需要准备一下编程环境。

  1. 计算机
    本文涉及的所有代码可以在没有显存的环境中执行。 我使用的机器配置为:

    • CPU: Intel i5-8400 2.80GHz
    • 内存: 16GB
  2. Visual Studio Code 和 venv
    这是很受欢迎的开发工具,相关文章的代码可以在 Visual Studio Code 中开发和调试。 我们用 pythonvenv 创建虚拟环境, 详见:
    在Visual Studio Code中配置venv。

  3. Ollama
    Ollama 平台上部署本地大模型非常方便,基于此平台,我们可以让 langchain 使用 llama3.1qwen2.5 等各种本地大模型。详见:
    在langchian中使用本地部署的llama3.1大模型 。

定义工具方法

下面定义了两个简单的工具方法:计算加法和乘法:

python">def create_tools():"""创建tools"""@tooldef add(x: int, y: int) -> int:"""计算a和b的和。"""print (f"add is called...{x}+{y}")return x + y@tooldef multiply(x: int, y: int) -> int:"""计算a和b的乘积。"""print (f"multiply is called...{x}*{y}")return x * ytools = [add, multiply]for t in tools:print("--")print(t.name)print(t.description)print(t.args)return toolstools = 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'}}

创建提示词

python">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 应该能通过这个完整的提示词生成工具方法的 参数(即:实参)了,我们后面试试看。

生成工具方法实参

定义测试方法:

python">def too_call(model_name,query):llm = ChatOllama(model=model_name,temperature=0.1,verbose=True)chain = prompt | llmmessage = chain.invoke({"input": query})print(f'response: \n{message.content}') 

lamma3.1deepseek 返回的结果为:

  • lamma3.1
{"name": "multiply","arguments": {"x": 3,"y": 12}
}
<think>
好,我现在需要解决用户的问题:“3乘以12等于多少?” 用户希望我作为助理,使用提供的工具来计算。首先,我要理解用户的需求是什么。...最后,我需要将这些信息整合成一个JSON对象,并确保语法正确,避免任何错误导致返回失败。这样,当用户调用这个工具时,就能得到正确的结果了。
</think>```json
{"name": "multiply","arguments": {"x": 3,"y": 12}
}
```

llama3.1deepseek-r1 都正确生成了工具方法的实参,只是 deepseek 包含了 <think>...</think> 块,后面需要将其中的 json 部分提取出来。

json 格式返回实参

自定义 JsonOutputParser

一般来说,结构化的数据在 链 中才好处理, JsonOutputParser 用于将结果转换为 json 格式,我们先自定义一个类,它继承自 JsonOutputParser 并能处理 deepseek 的返回文本。

python">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].texttext = 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 = textreturn super().parse_result(result, partial=partial)

上述方法使用正则表达式将 deepseek 返回的 json 内容提取出来,这样处理后,deepseek 就可以加入 langchain 中了。

返回 json

python">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}}

调用工具方法

定义通用方法

我们先定义一个通用的调用工具方法的方法:

python">class ToolCallRequest(TypedDict):"""invoke_tool 函数使用的参数格式。"""name: strarguments: 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 整合以上成果:

python">def invoke_chain(model_name,query):llm = ChatOllama(model=model_name,temperature=0.1,verbose=True)chain = prompt | llm | ThinkJsonOutputParser() | invoke_toolresult =chain.invoke({"input": query})print(f'invoke_chain:\n{result}')

调用此方法,我们会发现 llama3.1deepseek-r1 都返回了简单的结果:

36

返回结果中包含工具输入

返回工具输出和工具输入都很有帮助。我们可以通过 RunnablePassthrough.assign 输出,这将获取 RunnablePassthrough 组件的输入并为其添加一个键,同时仍传递当前输入中的所有内容。

python">def invoke_chain_with_input(model_name,query):llm = ChatOllama(model=model_name,temperature=0.1,verbose=True)from langchain_core.runnables import RunnablePassthroughchain = (prompt | llm | ThinkJsonOutputParser() | RunnablePassthrough.assign(output=invoke_tool))result = chain.invoke({"input": query})print(f'invoke_chain with input:\n{result}')

调用此方法,我们会发现 llama3.1deepseek-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

🪐祝好运🪐


http://www.ppmy.cn/devtools/157867.html

相关文章

【蓝耕元生代智算云平台】一键部署 DeepSeek人工智能模型

欢迎来到ZyyOvO的博客✨&#xff0c;一个关于探索技术的角落&#xff0c;记录学习的点滴&#x1f4d6;&#xff0c;分享实用的技巧&#x1f6e0;️&#xff0c;偶尔还有一些奇思妙想&#x1f4a1; 本文由ZyyOvO原创✍️&#xff0c;感谢支持❤️&#xff01;请尊重原创&#x1…

Http和Socks的区别?

HTTP 和 SOCKS 的区别 HTTP 和 SOCKS 都是用于网络通信的协议&#xff0c;但它们在工作原理、应用场景和实现方式上有显著的区别。以下是详细的对比和说明。 一、HTTP 协议 1. 定义 HTTP&#xff08;HyperText Transfer Protocol&#xff09;是用于传输超文本数据的应用层协…

MySQL——CRUD

一、Create新增数据行 语法&#xff1a; 其中&#xff0c;column表示要插入的列的列名&#xff0c;value_list表示列名对应的数据&#xff0c;且可以同时插入多行 一、单行数据 全列插入 全列插入可以不在表名student后面指定列名&#xff0c;也就是说&#xff0c;如果没有指定…

如何清理浏览器一段时间以前的缓存

浏览器缓存是浏览器为了提高网页加载速度而自动存储的数据&#xff0c;包括图片、脚本、样式表等文件。然而&#xff0c;随着时间的推移&#xff0c;这些缓存数据可能会占用大量磁盘空间&#xff0c;甚至影响浏览器的性能和稳定性。因此&#xff0c;定期清理浏览器缓存是一个良…

《IP-Adapter: 适用于文本到图像扩散模型的文本兼容图像提示适配器》学习笔记

paper:2308.06721 GitHub&#xff1a;tencent-ailab/IP-Adapter: The image prompt adapter is designed to enable a pretrained text-to-image diffusion model to generate images with image prompt. 目录 摘要 1、介绍 2、相关工作 2.1 文本到图像扩散模型 2.2 大模…

【vscode源码】如何编译运行vscode及过程中问题解决

Visual Studio Code&#xff08;VSCode&#xff09;作为一款流行的开源编辑器&#xff0c;市面上很多基于vscode的套壳APP&#xff0c;本文将详细介绍如何编译和运行VSCode的源码&#xff0c;并总结一些常见问题以及解决方案&#xff0c;帮助开发者顺利二次开发。 1. 准备工作(…

node.js使用mysql2对接数据库

一、引言 在现代Web开发中&#xff0c;Node.js作为一种高效、轻量级的JavaScript运行时环境&#xff0c;已经广泛应用于后端服务的开发中。而MySQL&#xff0c;作为一个广泛使用的关系型数据库管理系统&#xff08;RDBMS&#xff09;&#xff0c;提供了强大的数据存储和查询功能…

安卓基础(Okhttp3)

1️⃣ 添加 OkHttp 依赖 &#x1f4cc; 在 app/build.gradle 添加 OkHttp 依赖 dependencies {implementation com.squareup.okhttp3:okhttp:4.9.3 }2️⃣ 发送 GET 请求 &#x1f4cc; 发送一个 GET 请求 实例化okhttp客户端--->创建请求--->发送请求 import okhtt…