本文是 《从范式到工程:Plan & Execute + Nacos MCP 构建 AI Agent 的实践之路》 的姊妹篇。上一篇讲架构与设计决策,这一篇深入代码级,逐行追踪 agentic 服务的启动流程 和会话调用流程。两篇配合阅读效果最佳。
前置准备
项目地址:nacos-learn-example
本文聚焦一个 Python 模块:agentic/。建议打开源码配合阅读:
agentic/src/agentic/
├── __init__.py # 日志初始化 + 模块导出
├── api.py # ★ FastAPI 入口,启动流程 + 会话流程
├── config.py # 配置加载,.env → Settings
├── models.py # 数据模型:Conversation、Message、ExecutionPlan、PlanStep
├── conversation.py # 对话存储 CRUD
├── agent.py # ★ LLM 引擎,stream_llm 与 chat_stream
├── orchestrator.py # ★ 编排器:Plan → Validate → Execute → Summarize
├── planner.py # 规划阶段
├── validator.py # 校验阶段
├── executor.py # 执行阶段(含 summarize)
├── mcp_client.py # MCP SSE 客户端
└── tool_registry.py # 工具注册表:tool_name → server_name
一、启动流程:uvicorn agentic.api:app 之后发生了什么
1.1 全局时序总览
uvicorn 启动
│
├─ 1. 模块导入
│ └── __init__.py: 配置 logging
│
├─ 2. 加载配置
│ └── config.py: load_settings() → 读取 .env → Settings 对象
│
├─ 3. 创建全局单例 (模块级)
│ ├── McpRouterClient(settings.mcp_router_sse_url)
│ ├── Agent(api_key, base_url, model)
│ ├── ConversationStore()
│ ├── ToolRegistry()
│ └── Orchestrator(agent, tool_registry)
│
├─ 4. lifespan 启动 (api.py:47-82)
│ ├── 4.1 SSE 连接与握手
│ ├── 4.2 拉取元工具
│ ├── 4.3 发现 MCP 服务
│ └── 4.4 注册所有业务工具
│
└─ 5. yield → FastAPI 就绪
├── GET /api/health
├── POST /api/conversations
├── GET /api/conversations
├── GET /api/conversations/{id}
└── WS /api/ws
1.2 各阶段逐行拆解
步骤 2:配置加载(config.py)
python
# config.py:22-44
def load_settings(env_file: str | None = None) -> Settings:
# 查找 .env:优先 agentic/.env,回退项目根 .env
base = Path(__file__).resolve().parent.parent.parent # agentic/
env_candidates = [base / ".env", base.parent / ".env"]
# dotenv 读取环境变量
load_dotenv(env_file)
return Settings(
llm_api_key=os.getenv("LLM_API_KEY", ""),
llm_base_url=os.getenv("LLM_BASE_URL", "https://api.openai.com/v1"),
llm_model=os.getenv("LLM_MODEL", "gpt-4o-mini"),
mcp_router_sse_url=os.getenv("MCP_ROUTER_SSE_URL", "http://localhost:8083/sse"),
)
四个配置项:
LLM_API_KEY/LLM_BASE_URL/LLM_MODEL:OpenAI 兼容 API 的三要素MCP_ROUTER_SSE_URL:nacos-mcp-router 的 SSE 端点
步骤 3:全局单例初始化(api.py:28-39)
python
settings = load_settings()
mcp_client = McpRouterClient(settings.mcp_router_sse_url) # 仅创建对象,未连接
agent = Agent(api_key=..., base_url=..., model=...) # 未创建 AsyncOpenAI 客户端
store = ConversationStore() # 空的内存 dict
tool_registry = ToolRegistry() # 空的 tool_name → server_name 映射
orchestrator = Orchestrator(agent=agent, tool_registry=tool_registry)
# Orchestrator 内部创建:Planner(agent) + Validator(tool_registry) + Executor(agent)
这里的 延迟初始化 是重点:
McpRouterClient的构造函数只是保存了 URL,真正的 SSE 连接在 lifespan 中Agent的_client是None,直到第一次调用_get_client()才创建AsyncOpenAI
步骤 4:lifespan 启动(api.py:47-82)
这是启动流程的核心,用一张时序图来看:
lifespan(app)
│
├─ mcp_client.connect() ──→ SSE 连接
│ ├─ sse_client(url) → (read, write) stream
│ └─ ClientSession(read, write).initialize() ──→ MCP 协议握手
│
├─ mcp_client.list_tools() ──→ 获取 router 元工具
│ └─ result.tools → 写入 router_functions[]
│ ① search_mcp_server ── 搜索 Nacos 上注册的 MCP 服务
│ ② add_mcp_server ── 获取指定服务的工具定义
│ ③ use_tool ── 代理调用指定服务的工具
│
├─ mcp_client.discover_services() ──→ 发现业务服务
│ └─ 调用 router 的 search_mcp_server
│ 参数: {"task_description": "发现所有可用工具", "key_words": "all"}
│ 返回: markdown 文本,从中解析 JSON 提取服务名列表
│
├─ for each server_name:
│ │
│ └─ mcp_client.fetch_service_tools_via_add(name) ← 逐个拉取工具
│ └─ 调用 router 的 add_mcp_server
│ 参数: {"mcp_server_name": name}
│ 返回: markdown,解析其中 "tool 列表为: [...]" 的 JSON 数组
│ ↓
│ tool_registry.register(server, name, desc, schema)
│ └─ _tools[name] = (server, description, inputSchema)
│
├─ tool_registry.to_openai_functions()
│ └─ 转为 OpenAI function calling 格式,追加到 router_functions
│
└─ yield → 应用正式就绪
关键细节:router_functions 由两部分组成:
- 元工具 (3 个):
search_mcp_server、add_mcp_server、use_tool------来自mcp_client.list_tools() - 业务工具 (N 个):来自各 MCP 服务的工具定义------通过
tool_registry.to_openai_functions()批量生成
如果 router 连接失败 :代码 api.py:76-77 捕获所有异常,打印日志后继续启动。服务照样运行,只是没有 MCP 工具可用。
1.3 启动后的服务拓扑
┌──────────────────────────────────────────┐
│ agentic (FastAPI :8000) │
│ │
│ ┌──────────────────────────────────┐ │
│ │ router_functions (OpenAI 格式) │ │
│ │ ├── search_mcp_server (元工具) │ │
│ │ ├── add_mcp_server (元工具) │ │
│ │ ├── use_tool (元工具) │ │
│ │ ├── get_weather (业务工具) │ │
│ │ └── calculator (业务工具) │ │
│ └──────────────────────────────────┘ │
│ │
│ ┌──────── ToolRegistry ────────┐ │
│ │ get_weather → mcp-server-py │ │
│ │ calculator → mcp-server-py │ │
│ └──────────────────────────────┘ │
│ │
│ ┌─────── SSE 长连接 ─────────┐ │
│ │ → nacos-mcp-router:8083 │ │
│ └────────────────────────────┘ │
└──────────────────────────────────────────┘
二、会话模型:对话是怎么组织起来的
2.1 数据模型(models.py)
python
@dataclass
class Message:
role: str # "user" | "assistant"
content: str
timestamp: datetime
@dataclass
class Conversation:
id: str
title: str
messages: list[Message]
created_at: datetime
结构很简单:Conversation 包含一个 messages 列表,每次 chat 往列表追加消息。
2.2 存储实现(conversation.py)
python
class ConversationStore:
def __init__(self):
self._conversations: dict[str, Conversation] = {}
def create(self, title="新对话") -> Conversation:
conv = Conversation(id=str(uuid.uuid4()), ...)
self._conversations[conv.id] = conv
return conv
def get(self, conv_id) -> Conversation | None:
return self._conversations.get(conv_id)
def add_message(self, conv_id, role, content) -> Message | None:
conv = self.get(conv_id)
if not conv:
return None
msg = Message(role=role, content=content)
conv.messages.append(msg)
return msg
重要:这是内存存储。服务重启后所有对话丢失。生产环境需要替换为数据库持久化,但作为学习示例,这种实现让读者能清晰看到消息流的组织方式,不受持久化逻辑干扰。
2.3 REST API 操作(api.py)
| 端点 | 方法 | 功能 |
|---|---|---|
POST /api/conversations |
创建对话 | 返回 {id, title, created_at} |
GET /api/conversations |
列出对话 | 返回元数据列表,不含消息 |
GET /api/conversations/{id} |
获取对话 | 返回 {id, title, messages:[{role, content}]} |
这三个端点构成了完整的对话 CRUD,供前端 agentic-ui 使用。
三、WebSocket 流式对话协议
3.1 协议概览
WS /api/ws 是聊天的主入口,使用 JSON 消息协议:
客户端 ──send──→ 服务端: {"action": "chat", "conversation_id": "...", "message": "..."}
客户端 ──send──→ 服务端: {"action": "cancel", "conversation_id": "..."}
服务端 ──send──→ 客户端: {"type": "token", "data": {"token": "北"}}
服务端 ──send──→ 客户端: {"type": "token", "data": {"token": "京"}}
服务端 ──send──→ 客户端: {"type": "plan_start", ...}
服务端 ──send──→ 客户端: {"type": "plan_thinking", ...}
服务端 ──send──→ 客户端: {"type": "plan_step", ...}
服务端 ──send──→ 客户端: {"type": "plan_complete", ...}
服务端 ──send──→ 客户端: {"type": "step_start", ...}
服务端 ──send──→ 客户端: {"type": "step_thinking", ...}
服务端 ──send──→ 客户端: {"type": "tool_call", ...}
服务端 ──send──→ 客户端: {"type": "tool_result", ...}
服务端 ──send──→ 客户端: {"type": "step_complete", ...}
服务端 ──send──→ 客户端: {"type": "llm_request", ...}
服务端 ──send──→ 客户端: {"type": "token", ...}
服务端 ──send──→ 客户端: {"type": "done", "data": {"conversation_id": "..."}}
服务端 ──send──→ 客户端: {"type": "error", "data": {"message": "..."}}
3.2 并发管理(api.py:41)
python
_active_tasks: dict[str, asyncio.Task] = {}
每个 WebSocket 连接 + 对话的组合有一个 key:"{conn_key}:{conv_id}"。当同一个对话收到新的 chat 请求时,会先取消旧任务:
python
new_key = f"{conn_key}:{conv_id}"
if new_key in _active_tasks:
_active_tasks[new_key].cancel()
task = asyncio.create_task(run_stream())
_active_tasks[new_key] = task
取消动作也通过 cancel action 显式触发:
python
elif action == "cancel":
cancel_key = f"{conn_key}:{conv_id}"
if cancel_key in _active_tasks:
_active_tasks[cancel_key].cancel()
await websocket.send_json({"type": "done", ...})
3.3 tool_executor 闭包(api.py:174-188)
这是 WebSocket handler 中定义的一个闭包,是 LLM 调用工具的实际入口:
python
async def tool_executor(tool_name: str, args: dict) -> str:
entry = tool_registry.lookup(tool_name)
if entry:
# 业务工具:通过 router 的 use_tool 代理调用
server_name, _input_schema = entry
return await mcp_client.call_tool("use_tool", {
"mcp_server_name": server_name,
"mcp_tool_name": tool_name,
"params": json.dumps(args, ensure_ascii=False),
})
# 元工具:直接调用 router 的 search_mcp_server / add_mcp_server
return await mcp_client.call_tool(tool_name, args)
调用链路在 LLM 和 MCP 服务之间建立了两段路由:
LLM 决定调用 "get_weather"
↓
tool_executor("get_weather", {city: "北京"})
↓
ToolRegistry.lookup("get_weather") → ("mcp-server-py", inputSchema)
↓
mcp_client.call_tool("use_tool", {mcp_server_name, mcp_tool_name, params})
↓
nacos-mcp-router 转发给 mcp-server-py
↓
mcp-server-py 执行 get_weather("北京")
↓
结果一路返回给 LLM
四、四阶段编排详解
这是 agentic 最核心的设计。Orchestrator.run() 串联了 Planner → Validator → Executor → Summarizer。
4.1 Phase 1:Plan(规划阶段)
核心文件 :planner.py
目标:将用户自然语言请求拆解为结构化的步骤序列。
流程:
1. Planner.plan() 被调用
│
2. _build_compact_tool_list(registry) ← 工具精简列表(仅 name + description)
│ " • get_weather --- 查询指定城市的实时天气"
│ " • calculator --- 执行四则运算"
│
3. _build_planning_system_prompt(tool_list)
│ "你是一个任务规划器...不要执行任何操作..."
│
4. 构建 submit_plan 函数定义
│ submit_plan(understand, steps[], limitations)
│ steps[] 中的每个元素含: step, description, suggested_tool, reasoning
│
5. agent.chat_stream(messages, system_prompt, tools=[submit_plan])
│
6. LLM 流式输出两种事件:
├── "token": LLM 思考过程 → 前端显示 thinking
└── "tool_call": LLM 调 submit_plan 函数 → 结构化计划
│
7. 两种出口路径:
├── submit_plan 被调用 → _parse_plan() → ExecutionPlan
│ └── for each step: yield "plan_step" 事件
└── 未调用 submit_plan (纯聊天) → last_is_chat=True
└── yield "plan_complete" → 由 api.py 处理为 done
Planner 的 system prompt(planner.py:65-78):
你是一个任务规划器...不要执行任何操作,只做规划。
当前可用的工具及功能:
• get_weather --- 查询指定城市的实时天气
• calculator --- 执行四则运算
规划规则:
1. 每个步骤应该是原子操作,使用一个工具完成
2. 步骤之间按依赖关系排序
3. 如果某个步骤不需要工具,suggested_tool 设为 null
4. 如果某个任务无法用现有工具完成,在 limitations