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

源码分析mcp定义tools映射到LLM输入的过程

llm是如何获熟mcp tool定义的function函数,比如以下mcp示例的get_whether,又如何决定调用那个mcp function。

这里通过跟踪langchain_ollama、qwen_agent源代码进行分析。

from mcp.server.fastmcp import FastMCPmcp = FastMCP(name="weather",)@mcp.tool()
def get_weather(city: str) -> str:"""获取指定城市的天气信息"""# 简单模拟数据,实际应用中应该调用对应的APIweather_data = {"北京": "晴天,温度 22°C","上海": "多云,温度 25°C", "广州": "小雨,温度 28°C","深圳": "阴天,温度 26°C"}return weather_data.get(city, f"{city} 的天气数据暂不可用")@mcp.resource("resource://cities")
def get_cities():"""返回支持查询天气的城市列表"""cities = ["北京", "上海", "广州", "深圳"]return f"Cities: {', '.join(cities)}"@mcp.resource("resource://{city}/weather")
def get_city_weather(city: str) -> str:return f"Weather for {city}"

1 langchain agent

如下langchain agent代码所示,mcp定义的工具tools,是和模型model一起送入langchain agent,辅助agent响应用户问题。

from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from langchain_mcp_adapters.tools import load_mcp_tools
from langgraph.prebuilt import create_react_agent
import asyncioserver_params = StdioServerParameters(command="python", args=["server.py"])
async def run_agent():async with stdio_client(server_params) as (read, write):async with ClientSession(read, write) as session:await session.initialize()tools = await load_mcp_tools(session)agent = create_react_agent(model, tools)agent_response = await agent.ainvoke({"messages": [{"role": "user", "content": "上海天气如何?"},]})

2 langchain_ollama

这里model为ollama,最终调用langchain_ollama,实际为langchain_ollama/chat_models.py定义的ChatOllama(BaseChatModel)。

经过跟踪,tools保存在kwars参数中,最后在_acreate_chat_stream中,通过self._chat_params(messages,  stop, **kwargs)将tools参数融合在chat_params,再通过self.async_client直接调用ollama llm。

self._chat_params融合tools过程代码如下。

    def _chat_params(self,messages: list[BaseMessage],stop: Optional[list[str]] = None,**kwargs: Any,) -> dict[str, Any]:ollama_messages = self._convert_messages_to_ollama_messages(messages)if self.stop is not None and stop is not None:msg = "`stop` found in both the input and default params."raise ValueError(msg).....params = {"messages": ollama_messages,"stream": kwargs.pop("stream", True),"model": kwargs.pop("model", self.model),"think": kwargs.pop("reasoning", self.reasoning),"format": kwargs.pop("format", self.format),"options": Options(**options_dict),"keep_alive": kwargs.pop("keep_alive", self.keep_alive),**kwargs,}if tools := kwargs.get("tools"):params["tools"] = toolsreturn params

https://github.com/langchain-ai/langchain/blob/master/libs/partners/ollama/langchain_ollama/chat_models.py

langchain_ollama将messges和tools提交给ollama llm,并没有详细说明llm处理tools的过程。

3 llm如何处理tools

ollama使用go开发,代码组织和langchain_llama有较大不同,llm处理tools代码隐藏在细节中,不太好直接分析。

这里借用Qwen-Agent示例llm处理tools的过程,两者原理一致。

1)tools整合进system message

以下为Qwen-Agent处理tool工具函数的具体过程,代码链接如下。

https://github.com/QwenLM/Qwen-Agent/blob/main/qwen_agent/llm/fncall_prompts/qwen_fncall_prompt.py#L74

可见tool最终是被整合到system message中,这解释了为什么llm能看到这些tool,并能依据上下文决策合适tool的原因。

2)整合结果示例

参考网络资料,整合后输入到llm的messages示例如下

[Message({'role': 'system', 'content': '你是一个有用的帮手,可以使用合适的工具解决我的我问题
# 工具
## 你拥有如下工具:
### get_current_weather
get_current_weather: 基于给定的城市获取天气 输入参数:{"type": "object", "properties": {"location": {"type": "string", "description": "城市名称"}}, "required": ["location"]}
### get_current_time
get_current_time: 获取当前时间 输入参数:{}
## 你可以在回复中插入零次、一次或多次以下命令以调用工具:
✿FUNCTION✿: 工具名称,必须是[get_current_weather,get_current_time]之一。
✿ARGS✿: 工具输入
✿RESULT✿: 工具结果,需将图片用![](url)渲染出来。
✿RETURN✿: 根据工具结果进行回复'}),

Message({'role': 'user', 'content': '上海天气如何'})]
 

3)整合过程代码示例

整合过程代码如下

#*******************************tool function整合模版*******************************
FN_NAME = '✿FUNCTION✿'
FN_ARGS = '✿ARGS✿'
FN_RESULT = '✿RESULT✿'
FN_EXIT = '✿RETURN✿'
FN_STOP_WORDS = [FN_RESULT, FN_EXIT]FN_CALL_TEMPLATE_INFO_ZH = """# 工具## 你拥有如下工具:{tool_descs}"""FN_CALL_TEMPLATE_INFO_EN = """# Tools## You have access to the following tools:{tool_descs}"""FN_CALL_TEMPLATE_FMT_ZH = """## 你可以在回复中插入零次、一次或多次以下命令以调用工具:%s: 工具名称,必须是[{tool_names}]之一。
%s: 工具输入
%s: 工具结果
%s: 根据工具结果进行回复,需将图片用![](url)渲染出来""" % (FN_NAME,FN_ARGS,FN_RESULT,FN_EXIT,
)FN_CALL_TEMPLATE_FMT_EN = """## When you need to call a tool, please insert the following command in your reply, which can be called zero or multiple times according to your needs:%s: The tool to use, should be one of [{tool_names}]
%s: The input of the tool
%s: Tool results
%s: Reply based on tool results. Images need to be rendered as ![](url)""" % (FN_NAME,FN_ARGS,FN_RESULT,FN_EXIT,
)FN_CALL_TEMPLATE_FMT_PARA_ZH = """## 你可以在回复中插入以下命令以并行调用N个工具:%s: 工具1的名称,必须是[{tool_names}]之一
%s: 工具1的输入
%s: 工具2的名称
%s: 工具2的输入
...
%s: 工具N的名称
%s: 工具N的输入
%s: 工具1的结果
%s: 工具2的结果
...
%s: 工具N的结果
%s: 根据工具结果进行回复,需将图片用![](url)渲染出来""" % (FN_NAME,FN_ARGS,FN_NAME,FN_ARGS,FN_NAME,FN_ARGS,FN_RESULT,FN_RESULT,FN_RESULT,FN_EXIT,
)FN_CALL_TEMPLATE_FMT_PARA_EN = """## Insert the following command in your reply when you need to call N tools in parallel:%s: The name of tool 1, should be one of [{tool_names}]
%s: The input of tool 1
%s: The name of tool 2
%s: The input of tool 2
...
%s: The name of tool N
%s: The input of tool N
%s: The result of tool 1
%s: The result of tool 2
...
%s: The result of tool N
%s: Reply based on tool results. Images need to be rendered as ![](url)""" % (FN_NAME,FN_ARGS,FN_NAME,FN_ARGS,FN_NAME,FN_ARGS,FN_RESULT,FN_RESULT,FN_RESULT,FN_EXIT,
)FN_CALL_TEMPLATE = {'zh': FN_CALL_TEMPLATE_INFO_ZH + '\n\n' + FN_CALL_TEMPLATE_FMT_ZH,'en': FN_CALL_TEMPLATE_INFO_EN + '\n\n' + FN_CALL_TEMPLATE_FMT_EN,'zh_parallel': FN_CALL_TEMPLATE_INFO_ZH + '\n\n' + FN_CALL_TEMPLATE_FMT_PARA_ZH,'en_parallel': FN_CALL_TEMPLATE_INFO_EN + '\n\n' + FN_CALL_TEMPLATE_FMT_PARA_EN,
}def get_function_description(function: Dict, lang: Literal['en', 'zh']) -> str:"""Text description of function"""tool_desc_template = {'zh': '### {name_for_human}\n\n{name_for_model}: {description_for_model} 输入参数:{parameters} {args_format}','en': '### {name_for_human}\n\n{name_for_model}: {description_for_model} Parameters: {parameters} {args_format}'}tool_desc = tool_desc_template[lang]name = function.get('name', None)name_for_human = function.get('name_for_human', name)name_for_model = function.get('name_for_model', name)assert name_for_human and name_for_modelif name_for_model == 'code_interpreter':args_format = {'zh': '此工具的输入应为Markdown代码块。','en': 'Enclose the code within triple backticks (`) at the beginning and end of the code.',}else:args_format = {'zh': '此工具的输入应为JSON对象。','en': 'Format the arguments as a JSON object.',}args_format = function.get('args_format', args_format[lang])return tool_desc.format(name_for_human=name_for_human,name_for_model=name_for_model,description_for_model=function['description'],parameters=json.dumps(function['parameters'], ensure_ascii=False),args_format=args_format).rstrip()#*************************将tool信息整合进system message**************************
## 基于以上模版和组合函数,将tool信息整合进system message# Add a system prompt for function calling:tool_desc_template = FN_CALL_TEMPLATE[lang + ('_parallel' if parallel_function_calls else '')]tool_descs = '\n\n'.join(get_function_description(function, lang=lang) for function in functions)tool_names = ','.join(function.get('name_for_model', function.get('name', '')) for function in functions)tool_system = tool_desc_template.format(tool_descs=tool_descs, tool_names=tool_names)if messages and messages[0].role == SYSTEM:messages[0].content.append(ContentItem(text='\n\n' + tool_system))else:messages = [Message(role=SYSTEM, content=[ContentItem(text=tool_system)])] + messages

reference

---

langchain_ollama/chat_models.py

https://github.com/langchain-ai/langchain/blob/master/libs/partners/ollama/langchain_ollama/chat_models.py

qwen_fucall_prompt.py

https://github.com/QwenLM/Qwen-Agent/blob/main/qwen_agent/llm/fncall_prompts/qwen_fncall_prompt.py

详解大模型是如何理解并使用 tools ?

https://blog.csdn.net/2301_81940605/article/details/140063364

Qwen3

https://github.com/QwenLM/Qwen3

Qwen-Agent

https://github.com/QwenLM/Qwen-Agent

qwen2.5-max

https://qwenlm.github.io/blog/qwen2.5-max/

http://www.lryc.cn/news/622271.html

相关文章:

  • L4 级别自动驾驶 软件架构设计
  • Midjourney绘画创作入门操作
  • 二十四、Mybatis-基础操作-删除(预编译SQL)
  • 什么是可信空间的全域节点、区域节点、业务节点?
  • 基于STM32与边缘计算的工业设备异常振动预警系统设计
  • Pytest 插件使用指南:让你的测试更高效
  • 深度解读 Browser-Use:让 AI 驱动浏览器自动化成为可能
  • 如何回答研究过MQ的源码吗
  • week1-[顺序结构]大海
  • Centos7 服务管理
  • SpatialVLM和SpatialRGPT论文解读
  • ChatGPT-5(GPT-5)全面解析:一场从通用智能迈向专属智能的进化风暴,正在重塑人类与机器的协作边界
  • C#---StopWatch类
  • 【JAVA高级】实现word转pdf 实现,源码概述。深坑总结
  • 用matlab实现的svdd算法
  • 数据库(3)
  • Java学习day_12之面向对象进阶(抽象类接口内部类)
  • 【Golang】:流程控制语句
  • Java 正则表达式的使用方法
  • 开源长期记忆 短期记忆 框架调研对比19999字
  • 如何写好大模型的提示词prompt
  • 第40周——GAN入门
  • 【新手入门】Android基础知识(一):系统架构
  • 打靶日常-sql注入(手工+sqlmap)
  • 【Java EE进阶 --- SpringBoot】初识Spring(创建SpringBoot项目)
  • 本地生活|MallBook 分账赋能浙江本地生活服务平台,助力实现资金流转效率与合规性的双提升!
  • 一个集成多源威胁情报的聚合平台,提供实时威胁情报查询和播报服务、主动拦截威胁IP,集成AI等多项常用安全类工具
  • 超级云 APP 模式:重构移动互联网生态的新引擎
  • 高频量化详解,速度和程序化的满足!
  • QT|windwos桌面端应用程序开发,当连接多个显示器的时候,如何获取屏幕编号?