AIGC标识 计科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)获得图形界面。

posted @ 2026-10-21 14:00  段旷卓  阅读(2)  评论(0)    收藏  举报