|
| 1 | +import asyncio |
| 2 | +import json |
| 3 | +from openai import OpenAI |
| 4 | +from mcp.client.stdio import stdio_client |
| 5 | +from mcp import ClientSession, StdioServerParameters |
| 6 | + |
| 7 | +# 配置 vLLM 的 OpenAI 兼容 API |
| 8 | +OPENAI_API_KEY = "EMPTY" # vLLM 不需要真实密钥 |
| 9 | +OPENAI_API_BASE = "http://localhost:8000/v1" # vLLM 的 OpenAI 兼容接口地址 |
| 10 | + |
| 11 | +class MCPClientDemo: |
| 12 | + def __init__(self, server_path: str): |
| 13 | + self.server_path = server_path |
| 14 | + # ✅ 使用 vLLM 的 OpenAI 兼容接口 |
| 15 | + self.llm = OpenAI(api_key=OPENAI_API_KEY, base_url=OPENAI_API_BASE) |
| 16 | + |
| 17 | + async def run(self, user_query: str): |
| 18 | + server_params = StdioServerParameters(command="python", args=[self.server_path]) |
| 19 | + async with stdio_client(server=server_params) as (read_stream, write_stream): |
| 20 | + async with ClientSession(read_stream, write_stream) as session: |
| 21 | + await session.initialize() |
| 22 | + |
| 23 | + # 获取服务端注册的工具 |
| 24 | + tools = (await session.list_tools()).tools |
| 25 | + |
| 26 | + # 转换为 OpenAI 格式的 tools |
| 27 | + openai_tools = [] |
| 28 | + for tool in tools: |
| 29 | + openai_tools.append({ |
| 30 | + "type": "function", |
| 31 | + "function": { |
| 32 | + "name": tool.name, |
| 33 | + "description": tool.description or "", |
| 34 | + "parameters": tool.inputSchema or { |
| 35 | + "type": "object", |
| 36 | + "properties": { |
| 37 | + "city_name": {"type": "string", "description": "城市名称"} |
| 38 | + }, |
| 39 | + "required": ["city_name"] |
| 40 | + } |
| 41 | + } |
| 42 | + }) |
| 43 | + |
| 44 | + # ------------------------------- |
| 45 | + # 方法 1: vLLM + MCP 工具调用 |
| 46 | + # ------------------------------- |
| 47 | + # 第一步:让模型判断是否需要调用工具 |
| 48 | + system_message = f""" |
| 49 | + 你是一个智能助手。你可以使用以下工具: |
| 50 | + {json.dumps(openai_tools, ensure_ascii=False, indent=2)} |
| 51 | +
|
| 52 | + 如果用户的问题需要调用工具,请回复 "TOOL_CALL: <工具名> <JSON参数>"。 |
| 53 | + 如果不需要调用工具,请直接回答。 |
| 54 | + """ |
| 55 | + |
| 56 | + messages_for_tool_decision = [ |
| 57 | + {"role": "system", "content": system_message}, |
| 58 | + {"role": "user", "content": user_query} |
| 59 | + ] |
| 60 | + |
| 61 | + # ✅ 使用 vLLM(通过 OpenAI 客户端)获取决策 |
| 62 | + try: |
| 63 | + decision_response = self.llm.chat.completions.create( |
| 64 | + model="/home/ma-user/work/Qwen2.5-1.5B-Instruct/", # 模型名需与启动 vLLM 时一致 |
| 65 | + messages=messages_for_tool_decision, |
| 66 | + max_tokens=512, |
| 67 | + ) |
| 68 | + decision_text = decision_response.choices[0].message.content.strip() |
| 69 | + except Exception as e: |
| 70 | + decision_text = "" |
| 71 | + print(f"调用 vLLM 失败: {e}") |
| 72 | + |
| 73 | + result_with_tool = {"model_reply": "", "tool_called": None, "tool_result": None} |
| 74 | + |
| 75 | + # 检查是否需要调用工具 |
| 76 | + if decision_text.startswith("TOOL_CALL:"): |
| 77 | + try: |
| 78 | + # 解析工具调用指令 |
| 79 | + _, tool_name, args_json_str = decision_text.split(" ", 2) |
| 80 | + arguments = json.loads(args_json_str) |
| 81 | + |
| 82 | + # ✅ 通过 MCP 会话调用实际工具 |
| 83 | + tool_result = await session.call_tool(tool_name, arguments) |
| 84 | + |
| 85 | + # 第二步:将工具结果返回给模型,生成最终回复 |
| 86 | + messages_with_result = messages_for_tool_decision + [ |
| 87 | + {"role": "assistant", "content": decision_text}, |
| 88 | + {"role": "tool", "content": json.dumps(tool_result.model_dump(), ensure_ascii=False), |
| 89 | + "name": tool_name}, |
| 90 | + {"role": "user", "content": "请根据以上工具调用结果,回答用户的问题。"} |
| 91 | + ] |
| 92 | + |
| 93 | + final_response = self.llm.chat.completions.create( |
| 94 | + model="/home/ma-user/work/Qwen2.5-1.5B-Instruct/", |
| 95 | + messages=messages_with_result, |
| 96 | + max_tokens=512, |
| 97 | + ) |
| 98 | + result_with_tool["model_reply"] = final_response.choices[0].message.content |
| 99 | + result_with_tool["tool_called"] = tool_name |
| 100 | + result_with_tool["tool_arguments"] = arguments |
| 101 | + result_with_tool["tool_result"] = tool_result |
| 102 | + except Exception as e: |
| 103 | + result_with_tool["model_reply"] = f"工具调用解析错误: {e}。原始回复: {decision_text}" |
| 104 | + else: |
| 105 | + result_with_tool["model_reply"] = decision_text |
| 106 | + |
| 107 | + # ------------------------------- |
| 108 | + # 方法 2: 仅模型回复(无工具) |
| 109 | + # ------------------------------- |
| 110 | + try: |
| 111 | + response_no_tool = self.llm.chat.completions.create( |
| 112 | + model="/home/ma-user/work/Qwen2.5-1.5B-Instruct/", |
| 113 | + messages=[{"role": "user", "content": user_query}], |
| 114 | + max_tokens=512, |
| 115 | + ) |
| 116 | + message_no_tool = response_no_tool.choices[0].message |
| 117 | + result_no_tool = { |
| 118 | + "model_reply": message_no_tool.content |
| 119 | + } |
| 120 | + except Exception as e: |
| 121 | + result_no_tool = { |
| 122 | + "model_reply": f"调用 vLLM 失败(无工具): {e}" |
| 123 | + } |
| 124 | + |
| 125 | + return { |
| 126 | + "user_query": user_query, |
| 127 | + "with_mcp_tool": result_with_tool, |
| 128 | + "without_tool": result_no_tool |
| 129 | + } |
| 130 | + |
| 131 | + |
| 132 | +async def main(): |
| 133 | + client = MCPClientDemo(server_path="/home/ma-user/work/mcp/stdio_mcp.py") |
| 134 | + result = await client.run("nanjing的天气怎么样") |
| 135 | + |
| 136 | + print(">>> 用户提问:", result["user_query"]) |
| 137 | + print("\n【使用 MCP 工具】") |
| 138 | + print("模型回复:", result["with_mcp_tool"]["model_reply"]) |
| 139 | + if result["with_mcp_tool"]["tool_called"]: |
| 140 | + print("调用工具:", result["with_mcp_tool"]["tool_called"]) |
| 141 | + print("工具参数:", result["with_mcp_tool"]["tool_arguments"]) |
| 142 | + print("工具结果:", result["with_mcp_tool"]["tool_result"]) |
| 143 | + else: |
| 144 | + print("未调用任何工具") |
| 145 | + |
| 146 | + print("\n【不使用工具】") |
| 147 | + print("模型回复:", result["without_tool"]["model_reply"]) |
| 148 | + |
| 149 | + |
| 150 | +if __name__ == "__main__": |
| 151 | + asyncio.run(main()) |
0 commit comments