LLM MCP CLI Chat(Ollama + MCP 自主工具调用)
项目简介
本项目提供一个基于 Ollama 的多轮对话 CLI,并在其上实现了 MCP 工具体系:
- 支持显式工具指令(如
/time、/bmi) - 支持“模型自主决定是否调用 MCP 工具”的闭环:模型先给出 JSON 决策,程序执行工具并将结果回灌上下文,再让模型产出最终答复
安装与运行
依赖环境
- Python 3.9+
- 已安装并可访问的 Ollama 服务(默认
http://127.0.0.1:11434)
安装依赖
pip install -r requirements.txtrequirements.txt 中包含:
- requests:调用 Ollama HTTP API
- colorama:CLI 颜色输出
准备模型
ollama pull gemma3:1b启动程序
python main.py --model gemma3:1b可选参数:
python main.py --model gemma3:1b --host http://127.0.0.1:11434项目结构
main.py:命令行入口,读取用户输入并打印模型回复conversation_manager.py:对话编排与 MCP 路由(显式指令 / 自主调用 / 常规对话)mcp_handler.py:MCP 工具逻辑与执行,包含 BMI 解析、分类等ollama_client.py:与 Ollama 的 HTTP 接口封装README.md:说明文档
功能一览
- 多轮对话:完整保留上下文(system/user/assistant 消息)
- 显式 MCP 指令:
- /time 返回当前系统时间 - /bmi 身高(米) 体重(千克) 计算 BMI 并返回状态
- 自主 MCP 调用:
- 模型先输出仅含 JSON 的决策:{"use_tool": bool, "tool": "time|bmi", "args": {...}} - 程序按决策执行工具,将结果以“(来自MCP)”记录进对话历史 - 再次调用模型生成最终答复(可直接引用 MCP 结果)
MCP 工具创建流程(以 time / bmi 为例)
工具的核心在于“输入解析 + 业务计算 + 结构化输出”。本项目提供两种入口:
1. 显式指令入口(Slash Commands)
在 mcp_handler.py 中:
handle_mcp_command(text):识别以/开头的显式指令;/time:直接返回当前时间;/bmi:解析身高体重,计算 BMI 并做分级。
关键片段(简化示意):
# handle_mcp_command 内部要点
if text.lower() == "/time":
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
return MCPResult(handled=True, response=f"当前时间:{now}")
if text.lower().startswith("/bmi"):
height_m, weight_kg = parse_bmi_args(text)
bmi = weight_kg / (height_m * height_m)
return MCPResult(handled=True, response=f"BMI: {bmi:.2f},状态:{bmi_category(bmi)}")2. 程序化执行入口(execute_tool)
当模型决策需要调用工具时,conversation_manager.py 会调用 mcp_handler.execute_tool(tool, args):
# mcp_handler.execute_tool 要点
if tool in {"time", "/time"}: ...
if tool in {"bmi", "/bmi"}: ... # 接受 height_m/height 与 weight_kg/weight要新增工具,仅需:
- 在
execute_tool中添加该工具的名称分支与参数校验; - 在
handle_mcp_command中(可选)添加对应的显式指令解析; - 在 README 中记录工具名称、参数与返回格式。
让模型“自动调用 MCP”的思路与实现
设计思路
- 将“是否调用工具”的判断交给模型,但要求其以严格 JSON 给出决策,便于程序解析。
- 若模型建议调用工具:程序执行之,并把结果写回对话历史(以“(来自MCP)”开头),引导模型将其视为已可信事实。
- 再次询问模型生成最终、面向用户的自然语言答复。
实现要点(conversation_manager.py)
- 先走显式 slash 指令的快速通道:
- 命中后直接返回结果,并把“(来自MCP)”注入历史,供后续对话引用。
- 否则进入自主决策流程:
- 临时拼接一条 system 约束消息,要求模型“仅输出 JSON 决策结构”; - 解析模型返回的文本,提取首尾花括号之间的 JSON; - 若 use_tool == true,调用 execute_tool 执行; - 将工具结果以“(来自MCP)”记录到历史; - 再次请求模型,生成最终答复给用户。
伪代码(简化):
decision_text = chat([...system(JSON-only 指令), user_text])
obj = safe_json_parse(decision_text)
if obj.use_tool:
tool_result = execute_tool(obj.tool, obj.args)
add_assistant_message("(来自MCP)...结果:...")
reply = chat(history_with_mcp_result)
else:
reply = chat(history)JSON 决策格式
{
"use_tool": true,
"tool": "time" | "bmi",
"args": { "height_m": 1.75, "weight_kg": 65 },
"reason": "可选的原因说明"
}健壮性处理:程序会截取返回文本中的第一个 { 与最后一个 } 之间的子串再做解析,尽量容错。
交互示例
显式指令
你: /time
AI: 当前时间:2025-01-01 12:34:56自主调用
你: 我身高1.75米,体重65公斤,帮我看是否正常?
AI: (内部先给 JSON 决策 → 执行 bmi 工具 → 写入(来自MCP) → 生成最终答复)扩展建议
- 新增更多工具:查询数据库、调用外部 API、读写本地资源等
- 为工具结果添加结构化模式(如 JSON Schema),便于前端二次利用
- 在决策阶段加入少量示例(few-shot),提升模型决策稳定性
常见问题
- 模型输出的 JSON 不严格:已内置“首尾花括号截取”与异常捕获作为兜底。
- BMI 参数单位混淆:本项目约定身高单位为米、体重单位为千克,
execute_tool会做范围校验并报错提示。
版权与许可
仅供学习与研究使用。
