
1. 为什么要在本地把 Chainlit、LangGraph 和 MCP 串起来如果你已经能用 Ollama 跑通一个本地模型下一步大概率会卡在同一个地方模型只会聊天不会干活。让它读个文件、查个接口、算个数据就得自己写一堆 if-else 去解析输出越写越乱。Chainlit LangChain LangGraph MCP Ollama 这套组合解决的正是本地模型 可视化界面 可编排工具调用这件事。拆开看每个角色Ollama 负责在本地把 qwen2.5 这类模型跑起来数据不出机器LangChain 提供模型和工具的抽象层LangGraph 把 Agent 的思考—调工具—再思考画成一张有向图流程可控可追踪MCPModel Context Protocol用统一协议把外部工具接进来工具进程和主程序解耦Chainlit 则给你一个开箱即用的 Web 聊天界面流式输出、工具调用提示都能直接显示。这套东西适合谁适合想把本地模型接上真实工具、又不想从零写前端和调度逻辑的开发者。我试过纯手写 while 循环做工具调用状态一多就崩换成 LangGraph 之后流程清晰很多。下面从环境准备一路走到端到端跑通配置和代码都可以直接复制。2. TaoToken 统一 Key 的前置准备本地 Ollama 负责离线兜底但很多场景下本地 7B 模型在复杂推理、长上下文、工具参数生成上还是吃力。这时候需要一个能兼容 OpenAI 接口的云端模型作为补充让同一套 LangGraph 代码在 ollama 和云端之间切换。TaoToken 在这里的作用就是提供统一的 API Key 和兼容 OpenAI 的接入地址你不用为每个模型厂商单独维护一套鉴权和 base_url。接入前先在控制台创建 Key拿到sk-开头的字符串。这个 Key 同时能用于模型对话和后续的 Coding Plan 场景配置一次多处复用。相关入口模型对话体验https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmodel_chat控制台创建 Keyhttps://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentconsoleAPI Keys 管理https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentapi_keys接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentdocAPI 基础地址统一用https://taotoken.net/api注意这个地址后面不加任何查询参数。Key 不要硬编码进提交到仓库的文件用环境变量或本地.env读取这是后面配置骨架里会体现的一点。注意本地 Ollama 和云端模型是互补关系不是替代关系。断网、隐私敏感、低延迟场景走 Ollama需要更强推理和工具参数生成时切到云端。LangGraph 的图结构完全不用改只换 LLM 实例。3. 可复制的环境与配置骨架3.1 安装 Ollama 与拉取模型先确认本机 Ollama 服务在跑然后拉一个支持工具调用的模型。qwen2.5:7b 对工具调用支持较好显存占用也友好ollama pull qwen2.5:7b ollama listollama list能列出已加载模型就说明本地推理链路通了。如果拉取慢检查网络后重试即可不要用任何非官方加速手段。3.2 Python 依赖建议用虚拟环境隔离依赖装在一起python -m venv .venv source .venv/bin/activate pip install chainlit langchain-ollama langchain-openai langgraph langchain-mcp-adapters python-dotenvlangchain-mcp-adapters是把 MCP 工具转成 LangChain Tool 的关键包缺了它MultiServerMCPClient会导入失败。3.3 MCP 工具配置 mcp.jsonMCP Server 通过 stdio 启动LangChain 会自动拉起子进程你不需要手动开服务。示例配置{ mcpServers: { my_tools: { command: /usr/bin/python3, args: [/home/user/langchain-mcp-demo/pymcp.py] } } }command用绝对路径args指向你的 MCP Server 脚本。用相对路径时Chainlit 的工作目录可能和你预期不一致导致找不到脚本这是新手最常见的坑之一。3.4 统一 Key 的 settings.json 骨架Chainlit 支持用.chainlit/config.toml控制界面行为用环境变量注入 Key。下面这份config.toml骨架可以直接用[project] enable_telemetry false [features] unsafe_allow_html false [UI] name MCP Chat Agent [meta] generated_by 1.0Key 和 base_url 走环境变量在项目根目录建.envTAOTOKEN_API_KEYsk-你的key TAOTOKEN_BASE_URLhttps://taotoken.net/api OLLAMA_MODELqwen2.5:7b代码里用os.getenv读取这样本地调试和部署都不用手改源码。4. LangGraph MCP Agent 核心实现4.1 定义 StateLangGraph 用 State 在节点间传递数据对话场景下核心就是消息列表from typing import Annotated, TypedDict from langgraph.graph.message import add_messages class State(TypedDict): messages: Annotated[list, add_messages]add_messages负责把新消息追加而不是覆盖多轮对话靠它维持上下文。4.2 构建 Agent 图下面这段把模型、MCP 工具、条件路由和记忆串起来import asyncio import json import os from langchain_ollama import ChatOllama from langchain_openai import ChatOpenAI from langchain_mcp_adapters.client import MultiServerMCPClient from langgraph.graph import StateGraph, START from langgraph.prebuilt import ToolNode, tools_condition from langgraph.checkpoint.memory import MemorySaver class MCPAgent: def __init__(self, backend: str ollama): if backend ollama: self.llm ChatOllama( modelos.getenv(OLLAMA_MODEL, qwen2.5:7b), temperature0.7, streamingTrue, ) else: self.llm ChatOpenAI( modelqwen3-30b-a3b-instruct-2507, temperature0.7, api_keyos.getenv(TAOTOKEN_API_KEY), base_urlos.getenv(TAOTOKEN_BASE_URL, https://taotoken.net/api), streamingTrue, ) with open(mcp.json, r, encodingutf-8) as f: cfg json.load(f) client_config { name: { transport: stdio, command: s[command], args: s[args], } for name, s in cfg[mcpServers].items() } client MultiServerMCPClient(client_config) self.tools asyncio.run(client.get_tools()) self.llm self.llm.bind_tools(self.tools) self.graph self._build_graph() def _agent(self, state: State): return {messages: [self.llm.invoke(state[messages])]} def _build_graph(self): g StateGraph(State) g.add_node(agent, self._agent) g.add_node(tools, ToolNode(self.tools)) g.add_edge(START, agent) g.add_conditional_edges(agent, tools_condition) g.add_edge(tools, agent) return g.compile(checkpointerMemorySaver())几个关键点bind_tools把 MCP 工具注册给模型tools_condition自动判断模型输出里有没有工具调用请求有就走 tools 节点没有就结束MemorySaver按thread_id保存多轮状态。backend参数让你在 ollama 和云端之间一键切换图结构完全复用。4.3 Chainlit 前端集成Chainlit 的生命周期钩子负责把图的事件流渲染到界面import chainlit as cl cl.on_chat_start async def on_start(): cl.user_session.set(agent, MCPAgent(backendollama)) cl.user_session.set(thread_id, default) await cl.Message(contentMCP Chat Agent 已启动支持工具调用).send() cl.on_message async def on_message(message: cl.Message): agent cl.user_session.get(agent) thread_id cl.user_session.get(thread_id) msg cl.Message(content) await msg.send() async for event in agent.graph.astream_events( {messages: [(user, message.content)]}, config{configurable: {thread_id: thread_id}}, versionv2, ): etype event[event] if etype on_chat_model_stream: chunk event[data][chunk] if chunk.content: msg.content chunk.content await msg.update() elif etype on_tool_start: await cl.Message(contentf正在调用工具{event[name]}, authortool).send() elif etype on_tool_end: await cl.Message(content工具执行完成, authortool).send()astream_events的versionv2必须显式指定否则事件结构不一致on_chat_model_stream可能拿不到内容。工具调用过程用独立消息展示用户能直观看到 Agent 在干什么。5. 启动与验证工具调用链路5.1 启动 Chainlitchainlit run app.py -w-w开启热重载改代码自动重启。浏览器打开http://localhost:8000看到启动消息就说明前端通了。5.2 验证 MCP 工具是否被调用在输入框里发一个必须用工具才能回答的问题比如你的 MCP Server 里有个查天气的工具就问帮我查一下北京现在的天气。预期现象界面先出现正在调用工具xxx然后模型基于工具返回结果生成回答最后出现工具执行完成。如果只看到模型直接编答案、没有工具提示说明工具没被绑定或模型没触发调用往下看排查部分。5.3 切换云端模型验证统一 Key把on_chat_start里的backendollama改成backendtaotoken重启后同样发问题。如果云端链路正常回答质量会明显不同工具调用提示照常出现。这一步验证的就是 TaoToken 统一 Key 在 LangGraph 里和本地模型无缝互换。6. 本篇常见错误排查6.1 MultiServerMCPClient 导入失败报ModuleNotFoundError: No module named langchain_mcp_adapters说明依赖没装或装错环境。确认虚拟环境激活后重装pip install --upgrade langchain-mcp-adapters6.2 MCP Server 启动即退出现象是工具列表为空或日志里子进程立刻结束。多半是mcp.json里command或args路径不对。把command换成which python3的绝对路径args用脚本绝对路径。手动执行一次python3 /path/to/pymcp.py看能不能正常启动能启动再交给 LangChain 托管。6.3 模型不调用工具本地 7B 模型有时对工具描述不敏感。先确认self.tools非空再检查工具函数的 docstring 是否清晰——MCP 工具的 description 就是模型判断是否调用的依据。描述太模糊模型就倾向直接回答。换云端模型对比一下如果云端能调、本地不能就是模型能力问题不是代码问题。6.4 流式输出卡住或重复astream_events里如果同时处理了on_chat_model_stream和on_chat_model_end可能重复拼接内容。只保留 stream 分支累加即可。另外msg.update()调用过于频繁会有性能问题可以按字符数或时间间隔节流。6.5 多轮对话上下文丢失检查thread_id是否每次请求都一致。如果每次新建会话都换 idMemorySaver就找不到历史。生产环境把MemorySaver换成持久化 checkpointer否则进程重启状态就没了。7. 继续往下走的方向跑通这条链路之后比较自然的延伸有几个把 MCP Server 换成你自己业务里的工具比如查数据库、调内部接口给 Chainlit 加一个模型选择下拉框让用户在 ollama 和云端之间实时切换把MemorySaver换成 SQLite 或 Postgres 的 checkpointer让多轮状态跨重启保留。如果你打算把这套 Agent 用在长期编码或自动化任务上可以了解下 Coding Plan 场景统一 Key 在那边同样适用https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentcoding_plan需要更强模型做复杂工具编排时直接在模型对话里试效果https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmodel_chat配置和代码都在上面先跑通本地 Ollama 版本再切云端对比问题基本都能定位。