3207 字
16 分钟
开发随笔:一个简单的 Todo MCP,和一次关于减法的开发实践
2025-09-01
无标签

说是开发,其实绝大多数时间都在拷打Windsurf里的Claude、GPT和Gemini老师。

我们意念合一.jpg

一、MCP到底是什么#

MCP 是 Model Context Protocol 的缩写,最早由 Anthropic 提出。其作用可以理解为一种 「接口协议」,让 LLM 能够和外部的各种工具、数据库、服务进行交互。

但光看这一段定义,实际上我自己是很迷糊的,我想知道的是,一个只能预测文本tokens的模型,到底是如何调用一个外部API呢?

调用MCP的基本流程大概有五步:

第1步:准备阶段-向 LLM 提供工具调用说明

在用户与LLM开始对话之前,开发者会先将所有可用的工具(API、函数)按照特定格式(例如JSON Schema)定义好,并作为一条特殊的“系统指令”(System Prompt)或上下文的一部分,发送给LLM。

这些说明看起来像这样:

{
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "query_weather",
        "description": "获取指定城市实时的天气信息",
        "parameters": {
          "type": "object",
          "properties": {
            "city": {
              "type": "string",
              "description": "城市名称,例如:北京"
            }
          },
          "required": ["city"]
        }
      }
    },
    {
      "type": "function",
      "function": {
        "name": "book_flight_ticket",
        "description": "为用户预订从出发地到目的地的机票",
        "parameters": {
          "type": "object",
          "properties": {
            "departure_city": {"type": "string", "description": "出发城市"},
            "destination_city": {"type": "string", "description": "目的地城市"},
            "date": {"type": "string", "description": "出行日期,格式 YYYY-MM-DD"}
          },
          "required": ["departure_city", "destination_city", "date"]
        }
      }
    }
  ]
}

现在,LLM 已经知道了它有两个工具:query_weatherbook_flight_ticket,以及如何使用它们。

be like 给大家一本教辅资料,先看定义和例题,然后照葫芦画瓢去做练习题

第2步:分析并产生调用意图

用户输入一个指令,例如:“帮我查一下北京今天的天气怎么样?”

LLM在它庞大的语言知识和刚刚收到的工具说明书的共同作用下,进行推理:

  1. 用户的意图是“查询天气”。
  2. 我拥有的工具里,有一个叫 query_weather 的工具,它的描述是“获取指定城市实时的天气信息”,正好匹配。
  3. 这个工具需要一个 city 参数。
  4. 用户的输入里提到了“北京”,可以作为 city 参数的值。

因此,LLM 会生成一段文本,而这段文本恰好是一个结构化的 JSON,代表了它想要调用的工具和参数。LLM的输出会是这样的(这不是最终给用户的回答,而是一个中间步骤):

{
  "tool_calls": [
    {
      "id": "call_abc123", // 一个唯一的调用ID,用于后续匹配结果
      "type": "function",
      "function": {
        "name": "query_weather",
        "arguments": "{\"city\": \"北京\"}" // 参数被序列化为JSON字符串
      }
    }
  ]
}

第3步:MCP框架解析并执行

我们的应用程序后端(MCP框架或者别的什么代码)会接收到 LLM 在上一步生成的这个 JSON。

  1. 解析:程序看到 tool_calls 字段,知道这是一个工具调用请求。
  2. 定位:它根据 name: "query_weather" 找到本地真正定义的Python函数 def query_weather(city): ...
  3. 传参并执行:它解析 arguments 里的 "{\"city\": \"北京\"}",得到参数 city="北京",然后执行 query_weather("北京")
  4. 获取结果:这个函数可能会去调用一个真实的天气API,然后返回一个结果,比如:{"temperature": "25°C", "condition": "晴"}

第4步:结果返回 - 将执行结果喂回给LLM

MCP框架将第4步的执行结果,连同之前的调用ID,再次打包成一个结构化的信息,发送给LLM,以继续刚才的对话。

第5步:整合回答 - LLM生成最终的自然语言回复

LLM现在收到了新的信息。它会基于全部的上下文(原始问题 + 它的调用决策 + 工具的返回结果)来组织最终的回答。它看到:

  1. 用户问了北京天气。
  2. 我决定调用query_weather工具。
  3. 工具告诉我结果是 {"temperature": "25°C", "condition": "晴"}

于是,它生成了对用户友好的、最终的自然语言回答:“北京今天的天气是25摄氏度,晴天。”

二、MCP 的能与不能#

现在,我们知道了MCP的原理,那它到底能做什么,和做不了什么呢?

1.能#

目前 MCP 还只是一个被动调用的工具,但也确实极大地拓展了 LLM 的能力范围。回想一下,Funciton calling 其实早已存在,它是 MCP 的前身或者说技术雏形。MCP 的贡献在于将这一机制进一步规范化、标准化,使得不同模型、不同工具之间的交互能够更顺畅、更一致。

因此,MCP 给原本囿于纯文本生成的 LLM 打开了一扇通往外部真实世界的大门。通过各种search、hook、数据分析工具、GUI操作工具,MCP 让 LLM 具备了影响和改变外部环境的能力,实现了原本 LLM 想都不敢想的复杂流程。

除了基础工具端,目前的旗舰模型(GPT5、Gemini 2.5 Pro、Claude、DeepSeek V3.1等)的能力也确实已经足够好,能够比较可靠地理解工具定义、判断调用时机、生成正确参数,从而有效地执行各种工具调用,充当起了 agentic 模型的核心角色,这就是为什么有部分批评言论说manus实际上没有门槛,他们的实际业务核心是 anthropic 的 Claude 系列模型,而这些批评并不能说全无道理。

2.不能#

然而,MCP 作为一个被动的工具调用协议,其局限性同样也非常明显。很多我所希望的 LLM Agent 能够实现的智能效果,都或多或少被 MCP 的现有范式所束缚住了。

举个例子,MCP 的典型流程是用户提问 -> LLM 决定是否调用工具 -> 执行 -> 回答,这是一种强用户驱动的模式。LLM 很难主动发起一系列与当前用户 query 不完全直接相关但对达成更优结果有帮助的工具调用。一个科研助手,能否在用户提出一个初步想法后,主动去搜索相关文献、对比不同研究方法、甚至设计简单的实验验证假设?这在现有 MCP 框架下很难实现,因为它缺乏一个内在的、持续的“驱动力”去主动规划长期目标和探索路径。

但实际目前像 GPT5 Pro 和 Gemini 2.5 Pro UltraThink 的顶级旗舰推理模型也是可以通过更高的推理预算、更长的推理时间来部分达成上述效果的,只是目前这么做成本比较高,相对平民化的框架设计(Deepresearch和OpenAI的智能代理)的局限性和目标性又太明显太强。

MCP 本身也缺乏状态管理,难以维系长期上下文与任务记忆。虽然对话历史可以在一定程度上保留上下文,但对于一个需要执行多步骤、跨会话的复杂任务,Agent 如何追踪任务进度、保存中间结果、恢复中断的任务?MCP 本身是没有任何权限去控制约束system prompt和上下文的,没有规范如何让 Agent“记住”上次做到了哪一步,收集到了哪些信息,下一步计划是什么。这些都需要开发者在 MCP 之外额外构建复杂的状态管理系统

关于上下文,我在《浅谈ChatGPT的记忆实现机制 兼论工程端记忆设计》《关于酒馆SillyTavern所代表的伴侣模型系统的一些小思考》中亦有相关讨论,欢迎感兴趣的读者前去阅读。

最后,MCP 要求工具必须有清晰的预定义(如 JSON Schema)。对于一些接口不固定、参数含义动态变化,或者需要“创造性”使用的工具,这种强定义就显得非常僵化。Agent 无法像人类一样,面对一个新工具时,通过阅读模糊的文档、尝试错误来逐渐掌握其用法。它必须严格按照给定的“剧本”(工具定义)来“表演”。

三、简单的开发感悟#

在讨论了 MCP 宏大的技术原理和能力边界后,我想结合自己开发一个简单 Todo-MCP 的心路历程,分享一些更接地气的感悟。这或许能解释,为什么一个看似简单的协议,在实际落地时会面临诸多取舍。

1.最初的灵感:从project.md工作流开始#

我开发 Todo-MCP 的想法,源于我之前使用 Windsurf 或 Cursor 编写代码时养成的一个习惯。为了更好地约束 AI 编程助手并提升代码质量,我会在模型的System Prompt中告诉它:我们来共同维护一个名为 project.md 的文件。

在这个 Markdown 文件里,我会记录下当前的任务序列、未来的功能规划,以及已经完成的进度总结。我要求 AI 每次执行任务前,都必须先读取这个文件,以便充分了解项目的上下文和下一步计划。

当时我认为这种方法非常有效。它就像给了一个健忘但聪明的助手一本工作日志,让他随时都能“记起”我们要做什么、做到了哪里,从而使他的输出更有条理,避免在复杂的对话中迷失方向。我的初衷,就是希望将这种临时想到的工作流,用一个更稳定、更标准化的方式固定下来,在部分特化场景(如和AI一起学习)发挥作用——这便是 Todo-MCP 的思路来源。

2.为什么在开发中不断的砍功能?#

基于上述想法,我最初设计的 MCP 功能其实相当完整,大致包含了三个模块:

  1. 基础待办事项(Todo):这是最核心的功能,类似于现在很多编程助手在执行任务前会先列出具体步骤,完成后再逐一勾选。
  2. 笔记与日志(Notes):熟悉 Windsurf 的朋友可能会有共鸣——Windsurf 会维护一个包含待办、任务进展和笔记的 Markdown 文件。我希望能实现类似的效果,让 AI 能记录临时的想法和关键信息。
  3. 任务状态管理(State Management):例如,当一个任务临近到期时,由 AI 主动提醒我,并推动任务的完成。

然而,最终交付的产品,却是一个极其简洁的、只包含核心功能的 Todo-MCP,主要的考量如下:

当然最核心的原因肯定还是我太菜了+没有充足的开发时间

首先,技术实现过于臃肿,且缺乏基础设施支撑。 让模型通过 MCP 自主维护一个完整的 Markdown 文件,涉及到对文件内容部分或全部的增、删、改、查,这套流程对于一个轻量级插件来说太过重了。Windsurf 之所以能做得不错,是因为它背后有一整个 AI IDE 作为强大的基础设施(Infra)来支撑。而我的主要应用场景是跨平台的:PC 端的 Cherry Studio,加上手机端的 Rikka Hub,我需要和 AI 一起制定计划、学习知识。这个场景缺乏一个统一且完善的底层设施来承载复杂的交互。

其次,回归本质:一个持久化的“记忆组件”就已足够。 在反复权衡后,我发现一个简单的 Todo 组件就已经能满足我绝大多数的需求。我真正需要的,无非是一个能够跨越当前对话上下文的、持久化的状态管理和记忆组件。它需要比通用的记忆功能(如 Rikka 的记忆模块)更有条理、更方便管理,但其复杂度又不能远超于此。Todo 列表恰好完美地命中了这个甜点区。

最后,保持简洁,为未来探索保留可能性。 从复杂的构想回归到最核心的需求,本身也是一种明智的开发策略。先实现最核心、最高频的功能,确保其稳定可靠,远比构建一个庞大但处处是短板的系统要好。当然,这不代表放弃了最初的设想,或许在未来,我会基于这个小插件,逐步探索加上类似笔记和状态提醒的功能吧。

相关链接#

开发随笔:一个简单的 Todo MCP,和一次关于减法的开发实践
https://www.lapis.cafe/posts/ai-and-deep-learning/from-project-md-to-mcp-agent/
作者
时歌
发布于
2025-09-01
许可协议
CC BY-NC-SA 4.0