在使用 LLM(大语言模型) 调用工具方法时,可能会在运行时才会将类似 用户ID 的数值传递给工具。
大多数情况下,此类值不应由 LLM 控制。允许 LLM 控制 用户ID 可能会导致安全风险
相反,LLM 应该只控制本应由 LLM 控制的工具参数,而其他参数(如用户ID)应由应用程序逻辑固定。

本文将向您展示:如何防止大模型生成某些工具参数并在运行时直接注入它们。

本文使用 llama3.1MFDoom/deepseek-r1-tool-calling:7b 进行演练。 deepseek-r1 不支持 langchain 的 bind_tools 方法。

准备

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

  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.5deepseek 等各种本地大模型。详见:
    在langchian中使用本地部署的llama3.1大模型

定义工具方法

下面的工具方法将用于后面的测试:

user_to_pets = {}

@tool(parse_docstring=True)
def update_favorite_pets(
    pets: List[str], user_id: Annotated[str, InjectedToolArg]
) -> None:
    """添加喜爱的宠物列表。

    Args:
        pets: 喜爱的宠物列表。
        user_id: 用户 ID。
    """
    print(f'update_favorite_pets is called:{user_id}')
    user_to_pets[user_id] = pets


@tool(parse_docstring=True)
def delete_favorite_pets(user_id: Annotated[str, InjectedToolArg]) -> None:
    """删除喜爱的宠物列表。

    Args:
        user_id: 用户 ID。
    """
    print(f'delete_favorite_pets is called:{user_id}')
    if user_id in user_to_pets:
        del user_to_pets[user_id]


@tool(parse_docstring=True)
def list_favorite_pets(user_id: Annotated[str, InjectedToolArg]) -> None:
    """列出最喜欢的宠物(如果有)。

    Args:
        user_id: 用户 ID。
    """
    print(f'list_favorite_pets is called:{user_id}')
    return user_to_pets.get(user_id, [])
  • parse_docstring=True : 尝试从谷歌风格的函数文档字符串中解析出参数描述。
  • InjectedToolArg : 表示参数不由模型生成。

我们来看一下这些工具的输入,我们会看到 user_id 仍然会列出来:

update_favorite_pets.get_input_schema().model_json_schema()
{
    'description': '添加喜爱的宠物列表。', 
    'properties': {
        'pets': {'description': '喜爱的宠物列表。', 'items': {'type': 'string'}, 'title': 'Pets', 'type': 'array'}, 
        'user_id': {'description': '用户 ID。', 'title': 'User Id', 'type': 'string'}
    }, 
    'required': ['pets', 'user_id'], 
    'title': 'update_favorite_pets', 
    'type': 'object'
}

但是如果我们查看工具调用数据结构(即传递给模型进行工具调用的内容),user_id 已被删除:

update_favorite_pets.tool_call_schema.model_json_schema()
{
    'description': '添加喜爱的宠物列表。', 
    'properties': {
        'pets': {'description': '喜爱的宠物列表。', 'items': {'type': 'string'}, 'title': 'Pets', 'type': 'array'}
    }, 
    'required': ['pets'], 
    'title': 'update_favorite_pets', 
    'type': 'object'
}

测试工具调用

定义测试方法:

tools = [
    update_favorite_pets,
    delete_favorite_pets,
    list_favorite_pets,
]

def invoke_tool(model_name,query):
    """测试生成的tool_call"""

    llm = ChatOllama(model=model_name,temperature=0.1,verbose=True)
    llm_with_tools = llm.bind_tools(tools)

    ai_msg = llm_with_tools.invoke(query)
    print(f'result:\n{ai_msg.tool_calls}')

    return ai_msg

现在用 llama3.1MFDoom/deepseek-r1-tool-calling:7b 做一下测试,它们都能打印出以下内容:

[
    {
        'name': 'update_favorite_pets', 'args': {'pets': ['狗', '蜥蜴']}, 
        'id': 'e39422b2-0597-4271-b425-1458126f6087', 'type': 'tool_call'
    }
]

我做了3次测试,llama3.1 全部能输出上述结果; MFDoom/deepseek-r1-tool-calling:7b 则有1次输出错误。

在运行时注入 user_id

实现注入 user_id 的方法

我们先定义注入 user_id 的方法:在 AI消息 的 tool_call 中的参数中增加 user_id 信息,使用 @chain 声明可以让此方法加入到 链 中:

user_id ="u123"

@chain
def inject_user_id(ai_msg):
    tool_calls = []
    for tool_call in ai_msg.tool_calls:
        tool_call_copy = deepcopy(tool_call)
        tool_call_copy["args"]["user_id"] = user_id
        tool_calls.append(tool_call_copy)
    return tool_calls

我们用两个模型做一下测试,看能否正常注入 user_id:

query = "刘大军最喜欢的动物是狗和蜥蜴。"

def test_inject_user_id(model_name,query):
    ai_msg = invoke_tool(model_name,query)
    new_args = inject_user_id.invoke(ai_msg)
    print(f'inject_user_id:\n{new_args}')

执行上述测试方法,输出内容为:

[
    {
        'name': 'update_favorite_pets', 'args': {'pets': ['狗', '蜥蜴'], 'user_id': 'u123'}, 
        'id': '98ffadfc-a619-4a71-abfe-0bd5653e0d70', 'type': 'tool_call'
    }
]

创建工具执行链

下面我们将模型、注入用户ID代码和实际的工具链接在一起,创建工具执行链:

query = "刘大军最喜欢的动物是狗和蜥蜴。"

tool_map = {tool.name: tool for tool in tools}

@chain
def tool_router(tool_call):
    return tool_map[tool_call["name"]]

def execute_tool(model_name,query):
    """调用工具,返回结果"""

    llm = ChatOllama(model=model_name,temperature=0.1,verbose=True)
    llm_with_tools = llm.bind_tools(tools)

    # 将模型、注入用户ID代码和实际的工具链接在一起,创建工具执行链
    chain = llm_with_tools | inject_user_id | tool_router.map()

    result = chain.invoke(query)
    print(f'chain.invoke:\n{result}')
    print(f'now user_to_pets :\n{user_to_pets}')

上述代码中,通过 inject_user_id 可以生成注入了 user_id 的 tool_call 数组, tool_router.map() 则可以执行这些 tool_call。
用户的问题穿过 链 ,经过 链 中每个部分的处理后,两个模型最终输出了同样的结果:

[
    ToolMessage(content='null', name='update_favorite_pets', tool_call_id='ea8d3a9f-1cfc-4f88-b481-07c4d98b56f7')
]
{'u123': ['狗', '蜥蜴']}

标注参数的其它方法

除了通过函数文档内容标注工具函数的参数以外,还有其它的方法做到这一点:

class UpdateFavoritePetsSchema(BaseModel):
    """添加或者更新最喜爱的宠物列表。"""

    pets: List[str] = Field(..., description="最喜爱的宠物列表。")
    user_id: Annotated[str, InjectedToolArg] = Field(..., description="用户ID。")


@tool(args_schema=UpdateFavoritePetsSchema)
def update_favorite_pets(pets, user_id):
    user_to_pets[user_id] = pets
class UpdateFavoritePets(BaseTool):
    name: str = "update_favorite_pets"
    description: str = "添加或者更新最喜爱的宠物列表"
    args_schema: Optional[Type[BaseModel]] = UpdateFavoritePetsSchema

    def _run(self, pets, user_id):
        user_to_pets[user_id] = pets

update_favorite_pets = UpdateFavoritePets()
class UpdateFavoritePets(BaseTool):
    name: str = "update_favorite_pets"
    description: str = "添加或者更新最喜爱的宠物列表"

    def _run(self, pets: List[str], user_id: Annotated[str, InjectedToolArg]) -> None:
        user_to_pets[user_id] = pets

update_favorite_pets = UpdateFavoritePets()

总结

本文使用 llama3.1MFDoom/deepseek-r1-tool-calling:7b 演练了 LLM 调用工具时如何动态注册参数/实参,这在实际的应用场景中应该很常见。
另外,最后我们介绍了 python 标注参数的方法。如果涉及的参数多,貌似第二种方法最优雅。

代码

本文涉及的所有代码以及相关资源都已经共享,参见:

参考:

🪐感谢您观看,祝好运🪐