实践是最好的学习方式。为了深入理解 LangGraph 和模型上下文协议(MCP)服务器的生态,我们来从零开始构建一个 CLI 编码代理。我们的目标是,抛开 Claude Code 那些花里胡哨的功能,看看最基础的编码代理能做到什么程度。
那些商业编码代理往往会添加各种专有的"秘密配方"——特殊的上下文管理、精心设计的提示策略、优化过的工具选择算法。这些技术细节被包装得严严实实,很难搞清楚哪些是必需的,但对于学习来说,那些只是锦上添花。我们这篇文章的目标是验证一个问题:用最简单的方式让 LLM 在无限循环里不断调用工具,这样的"裸机"代理到底行不行?那些复杂的技术栈是真的必要吗,还是过度设计了?
核心功能设计
这个代理的设计理念就是极简但实用。整个系统由一个交互式状态图驱动,信息流向很清晰:用户输入 → 模型响应 → 工具调用 → 回到用户,形成持续的对话循环。本地功能实现了文件读取器和 pytest 的封装用于单元测试。MCP 集成覆盖了几个关键场景:Desktop Commander 负责文件系统操作,Pydantic AI 的沙箱 Python MCP 跑在 Deno Docker 容器里,DuckDuckGo 提供网络搜索,还有官方的 GitHub MCP。为了提升可观测性,还加入了丰富的终端界面和 Mermaid 工作流可视化,可以清楚地看到代理的思考和执行过程。
下面是几个使用示例:
通过 Desktop Commander MCP 的 read_file 工具读取 TypeScript 文件并展示内容
利用 DuckDuckGo 的 MCP 执行网络搜索
快速上手
环境准备很简单,只需要对 Docker 和命令行有基本了解就够了。启动主代理的命令:
uv run main.py如果要用沙箱 Python 执行功能,需要先构建 Deno MCP 的 Docker 镜像:
docker build -t deno-docker:latest -f ./mcps/deno/Dockerfile .启动后可以试试这些指令来体验功能:
- "Show me the content of main.py"
- "What tools do you have?"
- "Read requirements.txt"
状态持久化与调试
集成了
langgraph-checkpoint-sqlite来跟踪对话历史,方便调试。这个基于 SQLite 的检查点机制很适合本地实验场景。可以直接从终端查看代理状态:
sqlite3 checkpoints.db "SELECT * from writes LIMIT 2" sqlite3 checkpoints.db "SELECT * from checkpoints LIMIT 2"Pytest 集成
传统做法是写完代码手动跑测试,而现在代理会自动执行这个流程。体验提升非常明显:只需要说"X 功能有问题",代理就会跑测试套件,定位失败的测试用例,分析错误信息,然后给出针对性的修复方案。这彻底省掉了盯着测试日志发呆或者把终端输出复制到 ChatGPT 的繁琐操作,代理拿到的上下文信息足够精确,诊断效率大幅提高。
代码实现
这套代码基于 LangChain 框架,使用 Claude 3 Sonnet 作为核心模型来处理代码库的维护和开发任务。整个系统采用 StateGraph 工作流,包含三个关键节点:user_input、model_response 和 tool_use。
流程在节点间流转,模型响应决定下一步是调用工具还是回到用户输入环节。AgentState 负责维护状态,跟踪完整的消息历史。
核心工作流的实现代码:
# Key workflow setup from the Agent class def __init__(self): self.workflow = StateGraph(AgentState) # Register the three main nodes self.workflow.add_node("user_input", self.user_input) self.workflow.add_node("model_response", self.model_response) self.workflow.add_node("tool_use", self.tool_use) # Define the flow self.workflow.set_entry_point("user_input") self.workflow.add_edge("user_input", "model_response") self.workflow.add_edge("tool_use", "model_response") # Conditional routing based on tool usage self.workflow.add_conditional_edges( "model_response", self.check_tool_use, { "tool_use": "tool_use", "user_input": "user_input", }, )代理同时加载本地工具(比如单元测试)和运行在 Docker 容器里的 MCP 工具。工具设计上返回结构化的 ToolMessages,让 StateGraph 能够正确路由响应回模型。这些工具支持运行 Python 代码、搜索 DuckDuckGo、与 GitHub 交互等操作。状态在交互间通过 SQLite 检查点机制保持。
LangGraph 的状态架构解析
StateGraph 工作流机制值得单独拿出来细说,因为它是整个代理模式的基础支撑。LangGraph 的 StateGraph 实现了带持久化状态管理的有向图工作流。架构分解如下:
状态管理
class AgentState(BaseModel): messages: Annotated[Sequence[BaseMessage], add_messages]AgentState 类基于 Pydantic 的 BaseModel,维护完整对话历史,在图遍历过程中追踪所有消息类型(系统消息、用户消息、助手消息和工具消息)。
图结构设计
工作流由三个节点构成有向图:
- 用户输入节点:入口点,收集用户输入
- 模型响应节点:用 Claude 处理输入并决策下一步行动
- 工具使用节点:响应模型请求执行具体工具
图的配置代码:
# Core graph structure workflow = StateGraph(AgentState) # Node registration workflow.add_node("user_input", self.user_input) workflow.add_node("model_response", self.model_response) workflow.add_node("tool_use", self.tool_use) # Edge connections workflow.set_entry_point("user_input") workflow.add_edge("user_input", "model_response") workflow.add_edge("tool_use", "model_response")流程控制逻辑
图通过 check_tool_use 实现条件路由:
- user_input → model_response(固定路径)
- model_response → tool_use(存在工具调用时)
- model_response → user_input(无工具调用时)
- tool_use → model_response(固定路径)
持久化机制
通过 AsyncSqliteSaver 使用 SQLite 做检查点:
db_path = os.path.join(os.getcwd(), "checkpoints.db") self._checkpointer_ctx = AsyncSqliteSaver.from_conn_string(db_path) self.checkpointer = await self._checkpointer_ctx.__aenter__() self.agent = self.workflow.compile(checkpointer=self.checkpointer)这套机制让对话状态能跨会话保持遇到中断也能恢复。整个工作流构成一个持续循环,代理可以处理用户输入、生成响应、按需调用工具,在整个交互过程中保持上下文连贯性。
MCP 服务器的模块化实践
MCP 的配置用到了 LangChain 的 mcp-adapters 库,调用
get_tools方法。某些工具需要在系统提示词里补充描述信息,确保检索准确性。把 MCP 服务器打包成容器解决了环境冲突和依赖管理,用户直接跑容器就行,不用折腾安装配置。
Desktop Commander MCP提供的能力和 Claude Code 基本重合:完整的文件系统操作、代码编辑、审计日志。通过 Docker bind mount 配置把访问范围限制在测试文件夹,保持严格隔离。这种方式让实验变得安全,不用担心搞坏实际文件系统。
Pydantic AI 的 run-python MCP基于 Deno 容器创建沙箱 Python 执行环境。Deno 在 WebAssembly 沙箱里跑 Pyodide,阻止 LLM 在系统上执行危险代码或任意操作。虽然本地安装 Deno 也相对安全,但这会让本地环境变得臃肿。
DuckDuckGo MCP提供互联网搜索能力,需要实时信息检索时调用。
官方 GitHub MCP负责代码仓库搜索和管理功能。
实践中的几个关键问题
安全性和可配置性。对代理的访问权限必须有精细控制。可以给 MCPs 做 Docker 化并设置特定权限——比如 GitHub MCP 用只读标志运行来保护仓库。文件系统访问权限需要有简单的开关机制。,用
ghCLI 工具替代 GitHub MCP 可能更合理,能减少 token 消耗,和 Desktop Commander MCP 的终端访问配合也更流畅。随着 Claude Skills 推出和 CLI 工具链成熟,现在有更好的方案来防止上下文污染且保持功能完整。
工具过载是个要注意的问题。MCPs 加载所有工具及其描述至少两次:初始化代理时一次,每次用户查询时又一次。如果各种 MCPs 加起来有 40 多个工具,LLM 上下文很快就被塞满了。必须仔细检查每个 MCP 的工具命名,避免不同来源的函数名冲突。
MCP 资源使用是事件驱动的。MCPs 按需启动,资源消耗呈现尖峰特征,但在 MacBook Pro 上不算过分。编码代理需要从 MCP 客户端调用工具时,会用指定命令生成 MCP 进程——通常是
docker run,也可能是
npx或 Python 命令。容器运行时长刚好够执行工具调用,然后自行终止和清理,释放 CPU 资源给下一个操作。这种按需架构让代理保持轻量,同时能访问丰富的工具生态。
MCP 分发方式。很多 MCPs 提供预配置的 Docker 镜像,可以快速集成到编码代理里,方便维护隔离性。不过很多 MCPs 也有 Python 库版本,想要更紧密的集成且不需要额外安全层的话可以本地安装。
langchain-mcp 还不错。用
langchain-mcp-adapters库可以把 MCPs 桥接到 LangGraph,和 FastMCP 库的类似功能差不多水平。但它提供了平滑的接口,把 MCP 工具转换成 LangChain/LangGraph 兼容的工具,改动代码不多。
工具调用前的用户许可机制需要完善。代理执行工具不会提前征求许可,感觉风险挺大。可以在工具调用前加入人机交互环节能解决这个问题,但代价是会打断代理的执行流程。
后续改进方向
几个增强功能值得考虑。跨个人笔记的综合 RAG 工具——覆盖 Notion、Obsidian、文本文件、markdown——能让代理访问多年积累的知识和研究资料。加入 Confluence MCP 或内部文档 CLI 可以快速搜索公司特定的实践规范。如前面提到的,换成
ghCLI 替代 GitHub MCP 能节省 token。最后在每次工具调用前实现人机交互中断会增加关键的安全层,不过希望做成可配置的,避免在可信操作时频繁打断代理。