在使用 LLM(大语言模型)
调用工具方法时,可能会在运行时才会将类似 用户ID 的数值传递给工具。
大多数情况下,此类值不应由 LLM
控制。允许 LLM
控制 用户ID 可能会导致安全风险。
相反,LLM
应该只控制本应由 LLM
控制的工具参数,而其他参数(如用户ID)应由应用程序逻辑固定。
本文将向您展示:如何防止大模型生成某些工具参数并在运行时直接注入它们。
本文使用
llama3.1
和MFDoom/deepseek-r1-tool-calling:7b
进行演练。deepseek-r1
不支持langchain
的 bind_tools 方法。
准备
在正式开始撸代码之前,需要准备一下编程环境。
-
计算机
本文涉及的所有代码可以在没有显存的环境中执行。 我使用的机器配置为:- 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大模型 。
定义工具方法
下面的工具方法将用于后面的测试:
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.1
和 MFDoom/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.1
和 MFDoom/deepseek-r1-tool-calling:7b
演练了 LLM
调用工具时如何动态注册参数/实参,这在实际的应用场景中应该很常见。
另外,最后我们介绍了 python
标注参数的方法。如果涉及的参数多,貌似第二种方法最优雅。
代码
本文涉及的所有代码以及相关资源都已经共享,参见:
参考:
🪐感谢您观看,祝好运🪐