计科8班小组合作作业--构建一个能说会做的智能体 --Agent说明文档
校园小助手 · MCP 智能体 —— Agent 说明文档
项目 | 内容 |
|---|---|
项目名称 | 校园小助手(campus-mini-agent) |
技术栈 | Python 3.9+ / Model Context Protocol(stdio + JSON-RPC 2.0)/ DeepSeek Chat Completions(OpenAI 兼容) |
依赖 | 无第三方库,仅标准库 |
代码规模 | 6 个源文件 + 1 个测试文件,约 900 行 |
测试 | 20 项离线单元测试全部通过 |
一、需求描述
1.1 背景
学生每天被“记事情、算数、查天气、写点东西”这类小任务打断,分散在备忘录、计算器、天气 App、对话窗口之间,切换成本高。我们希望有一个入口,说一句人话就把事办了。
1.2 目标
构建一个具备“能说 + 会做”特征的智能体:
- 能说:接受自然语言输入,理解意图,用中文简洁回复;
- 会做:识别出需要执行的动作后,调用工具真正落地(写文件、算数、查接口),而不是“嘴上说已完成”。
1.3 功能需求
编号 | 需求 | 验收标准 |
|---|---|---|
F1 | 待办管理 | 能新增、查看、标记完成;重启后数据仍在 |
F2 | 四则运算 | 支持括号、乘方、整除、取模;非法表达式给出错误提示而非崩溃 |
F3 | 天气查询 | 给定城市返回当前温度、天气、体感、湿度 |
F4 | 笔记读写 | 把内容写成 Markdown 文件,并能读回 |
F5 | 多轮对话 | 保留最近几轮上下文;支持 :reset 清空 |
F6 | 一键演示 | --demo 跑固定脚本,便于验收与截图 |
1.4 非功能需求
编号 | 需求 | 做法 |
|---|---|---|
N1 | token 消耗尽量少 | 本地路由优先,确定指令 0 token;历史裁剪;提示词极简 |
N2 | 可离线降级 | 无 API Key 时仍可执行确定性指令,明确告知处于离线模式 |
N3 | 安全 | 计算不用 eval;文件工具限制目录;Key 不入库 |
N4 | 可移植 | 纯标准库,任何装了 Python 的机器 python run.py 即可 |
N5 | 可测试 | 网络与大模型都被隔离,20 项测试离线跑通 |
1.5 范围之外
不做图形界面、不做多用户账号体系、不做向量数据库记忆——本次作业聚焦“工具调用闭环”这一件事,把架构留出扩展位即可。
二、业务流描述
2.1 总体流程(两级路由)
┌──────────────────────┐
用户自然语言输入 → │ agent/router.py 正则 │
└──────────┬───────────┘
命中 │ │ 未命中
▼ ▼
┌─────────────────┐ ┌────────────────────────┐
│ 直接调 MCP 工具 │ │ agent/brain.py 请求 LLM│
│ (0 token) │ │ 携带 tools/list 工具表 │
└────────┬────────┘ └───────────┬────────────┘
│ ▼
│ ┌──────────────────────┐
│ │ 模型返回 tool_calls? │
│ └───────┬──────────────┘
│ 是 │ │ 否
│ ▼ ▼
│ ┌────────────────┐ ┌──────────────┐
│ │ MCP tools/call │ │ 直接生成回复 │
│ │ 结果回填模型 │ └──────┬───────┘
│ └───────┬────────┘ │
└──────────────┴──────────────────┘
▼
统一格式化输出
为什么分两级:像“算一下 (128+72)*3/4”“看看我的待办”这类句子,语法完全确定,让大模型去“理解”纯属浪费;而“明天去广州开会,那边热不热”需要理解指代(“那边”=广州)和意图(查天气),必须交给模型。第一级判断“是否确定”,第二级处理“不确定”,这就是本项目省 token 的核心。
2.2 一次“查天气”的完整时序
用户 CLI Agent MCP 服务器 外部接口 / 模型
│ │ │ │ │
│─输入─▶│ │ │ │
│ │─handle()─────▶│ │ │
│ │ │─route() 未命中──┐ │
│ │ │◀────────────────┘ │
│ │ │──chat(tools)──────────────────────▶│
│ │ │◀──tool_calls: weather(广州)─────────│
│ │ │──tools/call────▶│ │
│ │ │ │──HTTPS──────────▶│
│ │ │ │◀──JSON───────────│
│ │ │◀──"广州 33.9°C…"│ │
│ │ │──tool 结果回填──────────────────────▶│
│ │ │◀──"广州现在 33.9°C,晴…"─────────────│
│ │◀──reply───────│ │ │
│◀─打印─│ │ │ │
2.3 分支与异常
场景 | 处理 |
|---|---|
没配 API Key | 进入离线模式,只执行确定性指令,并提示可用工具 |
模型接口报错/超时 | 重试 2 次;仍失败则给可读提示,不崩溃 |
工具内部异常 | 服务器捕获后返回 isError + 错误文本,模型据此解释 |
模型连续调工具不收敛 | 最多 3 轮工具循环,超限给兜底话术 |
天气接口不可达 | open-meteo 失败转 wttr.in,两者都失败返回“暂时查不到” |
非法文件路径 | ../、绝对路径、子目录、超长文件名一律拒绝 |
三、实现说明
3.1 目录结构
mcp-mini-agent/
├── run.py 入口(交互 / --demo / --once)
├── agent/
│ ├── cli.py 命令行界面、演示脚本、.env 解析
│ ├── brain.py DeepSeek 客户端 + 工具调用循环 + token 统计
│ ├── router.py 本地零 token 意图路由(正则)
│ └── mcp_client.py MCP 客户端:拉起子进程、握手、tools/list、tools/call
├── mcp_server/
│ ├── server.py MCP 服务器:stdio + JSON-RPC 2.0
│ └── tools.py 7 个工具 + 工具清单(inputSchema)
├── tests/test_all.py 20 项离线测试
└── data/ 运行时数据(待办 JSON、笔记 Markdown)
3.2 为什么用 MCP
作业推荐 MCP。它的价值在于把“能力”和“大脑”解耦:工具跑在独立进程里,通过标准协议暴露给任何 MCP 客户端(Claude Desktop、Cherry Studio 等)。本项目因此可以只写一次工具,既给自己用,也能挂到别的客户端上。
为了让它“零依赖能跑”,我们按 MCP 规范(2024-11-05)实现了最小子集,而不是依赖官方 SDK:
方法 | 方向 | 作用 |
|---|---|---|
initialize | 客户端 → 服务器 | 协商 protocolVersion,取 serverInfo |
notifications/initialized | 客户端 → 服务器 | 通知,无响应 |
tools/list | 客户端 → 服务器 | 返回工具名、描述、JSON Schema |
tools/call | 客户端 → 服务器 | 执行工具,返回 content[].text |
真实报文(stdout 一行一条 JSON):
{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"calc","arguments":{"expression":"(128+72)*3/4"}}}
{"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"(128+72)*3/4 = 150"}],"isError":false}}
因为 stdout 是协议通道,所有日志都写 stderr——这是写 MCP 服务器最容易踩的坑。
3.3 工具清单
工具 | 参数 | 实现要点 |
|---|---|---|
todo_add | text | 追加到 data/todos.json,自增编号 |
todo_list | — | 只回显最近 20 条,控制上下文长度 |
todo_done | id | 幂等:已完成再点一次给提示 |
calc | expression | ast 白名单求值,禁函数调用/属性访问,限制指数 ≤64、结果 ≤1e15 |
weather | city | open-meteo 地理编码 + 实时天气,失败转 wttr.in |
file_write | name, content | 限制在 data/files/,自动补 .md |
file_read | name | 读取并截断到 800 字 |
3.4 大模型调用
- 接口:POST https://api.deepseek.com/chat/completions,模型 deepseek-chat,OpenAI 兼容格式。
- 只用 urllib,不引 openai SDK,减少安装负担。
- 工具清单由 mcp_client.openai_tools() 把 MCP 的 inputSchema 直接转成 function.parameters,一处定义,两处复用。
- 工具调用循环:
for _ in range(max_tool_rounds): # 最多 3 轮
resp = llm.chat(messages, tools)
if not resp["tool_calls"]:
return final_text # 模型给最终回复
messages.append(assistant_with_tool_calls)
for call in resp["tool_calls"]:
text = mcp.call_tool(call.name, call.args)
messages.append(tool_message(text)) # 结果回填
3.5 省 token 的具体做法
手段 | 效果 |
|---|---|
本地路由优先 | 5/8 轮完全不调模型,0 token |
系统提示词 60 字 | 约 40 tokens,比“角色+能力+格式”三段式省一半 |
工具描述一行 | 7 个工具的 schema 约 200 tokens |
历史只留 3 轮 + 不存工具报文 | 上下文不随轮次线性膨胀 |
max_tokens=256, temperature=0 | 回复短且稳定 |
工具输出截断 600 字 | 防止读大文件把上下文撑爆 |
3.6 安全与健壮性
1. 不用 `eval`:calc 用 ast 白名单递归求值,__import__('os').system('dir') 直接返回错误。
2. 文件沙箱:_safe_path() 拒绝 ..、/、绝对路径、超长名,并二次校验 resolve() 后仍在 data/files 下。
3. 原子写:待办先写 .tmp 再 replace,进程被杀也不会写坏 JSON。
4. 密钥管理:Key 只在 .env,.gitignore 已排除,仓库只提交 .env.example。
5. 进程隔离:MCP 服务器是独立子进程,工具崩溃不会带走主对话。
3.7 测试
python -m unittest discover -s tests -v
覆盖:计算器(4 类)、待办(3 类)、文件(3 类)、路由(7 类)、MCP 协议(1 项端到端)、离线降级(1 项)。全部离线,不消耗 API 额度。
四、使用示例(功能演示)
4.1 运行
cp .env.example .env # 填 DEEPSEEK_API_KEY
python run.py --demo
4.2 真实运行日志(本地路由开启)
==============================================================
校园小助手 · MCP 智能体(会说 + 会做)
MCP 服务器:campus-mini-agent v1.0.0
工具数量:7 模型:deepseek-chat
==============================================================
[01] 我:帮我记一条待办:周五 23:00 前交软件工程实验报告
↳ 调用工具 todo_add(text='周五 23:00 前交软件工程实验报告')
↳ 路径:本地路由(0 token),本轮 0 tokens
小助手:已添加待办 #1:周五 23:00 前交软件工程实验报告(当前共 1 条未完成)
[02] 我:看看我的待办
↳ 调用工具 todo_list()
↳ 路径:本地路由(0 token),本轮 0 tokens
小助手:待办(未完成 1 条):
[ ] #1 周五 23:00 前交软件工程实验报告
[03] 我:把第1条标记完成
↳ 调用工具 todo_done(id=1)
↳ 路径:本地路由(0 token),本轮 0 tokens
小助手:已完成 #1:周五 23:00 前交软件工程实验报告
[04] 我:算一下 (128+72)*3/4
↳ 调用工具 calc(expression='(128+72)*3/4')
↳ 路径:本地路由(0 token),本轮 0 tokens
小助手:(128+72)*3/4 = 150
[05] 我:明天要去广州开会,那边现在热不热?
↳ 调用工具 weather(city='广州')
↳ 路径:大模型 + 工具调用,本轮 1643 tokens
小助手:广州现在 33.9°C,晴,体感 37.5°C,挺热的,注意防晒补水。
[06] 我:帮我把今天的组会要点写成笔记:确定选题为校园智能助理,下周三前完成原型
↳ 调用工具 file_write(name='组会要点', content='组会要点\n1. 确定选题:校园智能助理\n2. 下周三前完成原型')
↳ 路径:大模型 + 工具调用,本轮 1730 tokens
小助手:已写入笔记「组会要点.md」:选题为校园智能助理,下周三前完成原型。
[07] 我:读一下组会要点
↳ 调用工具 file_read(name='组会要点')
↳ 路径:本地路由(0 token),本轮 0 tokens
小助手:组会要点
1. 确定选题:校园智能助理
2. 下周三前完成原型
[08] 我:你好,你都能做什么?
↳ 路径:大模型 + 工具调用,本轮 800 tokens
小助手:我能管待办(增删查)、算数、查天气、读写笔记。需要哪样?
===== 演示结束 =====
模型调用 5 次 | 输入 3990 + 输出 183 = 4173 tokens(缓存命中 2944) | 本地路由命中 5 次 | 工具调用 7 次
4.3 省 token 的对照实验
同一天、同一台机器、同一份 8 轮脚本,只把本地路由关掉(--no-router):
指标 | 本地路由开启(默认) | 全部交给大模型 | 变化 |
|---|---|---|---|
模型调用次数 | 5 | 15 | −67% |
输入 tokens | 3 990 | 11 571 | −66% |
输出 tokens | 183 | 455 | −60% |
合计 tokens | 4 173 | 12 026 | −65% |
平均每轮 | 522 | 1 503 | −65% |
结论:把“确定性”留在本地,是本项目最划算的一步优化——同样的任务,token 只要三分之一。
4.4 交互式使用
我:算一下 2**10
↳ 调用工具 calc(expression='2**10')
小助手:2**10 = 1024
我::stats
模型调用 0 次 | 输入 0 + 输出 0 = 0 tokens | 本地路由命中 1 次 | 工具调用 1 次
我::tools
- todo_add: 添加一条待办
- todo_list: 列出待办清单
...
4.5 作为通用 MCP 服务器复用
{
"mcpServers": {
"campus-mini-agent": {
"command": "python",
"args": ["-m", "mcp_server.server"],
"cwd": "<项目绝对路径>"
}
}
}
把它填进 Claude Desktop 的配置文件,todo_add 等 7 个工具会直接出现在对方客户端的工具面板里——这正是 MCP “写一次,到处能用”的意义。
五、已知限制与后续计划
1. 路由规则是手写的:只覆盖 6 类高频表达,换说法会落到大模型。后续可用小模型或 embedding 做意图分类,把命中率提上去。
2. 记忆只保留 3 轮:跨天任务需要长期记忆,可加一个“记忆”工具,把要点写进 data/memory.md。
3. 单机单用户:待办文件没有并发控制,多用户场景需要换 SQLite + 锁。
4. 只有 CLI:可以复用同一套 Agent,加一层 Web(Flask/FastAPI)获得图形界面。

浙公网安备 33010602011771号