当前位置: 首页 > article >正文

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

文章目录

    • 准备
    • 定义工具方法
    • 创建提示词
    • 生成工具方法实参
    • 以 `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大模型 。

定义工具方法

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

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.1deepseek 返回的结果为:

  • 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.1deepseek-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.1deepseek-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.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.kler.cn/a/539125.html

相关文章:

  • 使用PyCharm进行Django项目开发环境搭建
  • 除了webpackPrefetch,还有什么其他预加载组件的方法?
  • 【04】Java+若依+vue.js技术栈实现钱包积分管理系统项目-若依框架二次开发准备工作-以及建立初步后端目录菜单列-优雅草卓伊凡商业项目实战
  • Nginx部署Umi React前端项目标准配置
  • 问题大集04-浏览器阻止从 本地 发起的跨域请求,因为服务器的响应头 Access-Control-Allow-Origin 设置为通配符 *
  • Windows 本地部署大模型 OpenWebUI+Ollama
  • Java实现状态模式
  • Linux sysfs虚拟文件系统
  • springboot主要有哪些功能
  • 多租户架构设计与实现:基于 PostgreSQL 和 Node.js
  • 激活函数篇 04 —— softmax函数
  • windows + visual studio 2019 使用cmake 编译构建静、动态库并调用详解
  • C# 封送和远程编程介绍
  • 消息编号BK062 请给会计事项RKS设置一数字域
  • AI大模型,我选本地部署国产开源DeepSeek
  • json格式化html
  • HTML开发常见错误排查技巧与浏览器兼容性解决方案
  • Java 大视界 -- Java 大数据在智能政务中的应用与服务创新(78)
  • Linux高并发服务器开发 第十六天(execlp/execl 进程回收/孤儿进程/僵尸进程 wait/waitpid回收 进程间的通信)
  • 【BUUCTF杂项题】刷新过的图片
  • [8-2-2] 队列实验_多设备玩游戏(红外改造)_重录
  • LLMs之DeepSeek r1:TinyZero(复现 DeepSeek R1 Zero 的核心功能)的简介、安装和使用方法、案例应用之详细攻略
  • SpringBoot 接口内容加密方案(RSA+AES+HMAC校验)认知
  • Python基础语法精要
  • 笔记:理解借贷相等的公式
  • Linux debugfs虚拟文件系统