agentic 源码深度拆解:启动流程与会话调用流程全解

本文是 《从范式到工程: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_clientNone,直到第一次调用 _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 由两部分组成:

  1. 元工具 (3 个):search_mcp_serveradd_mcp_serveruse_tool------来自 mcp_client.list_tools()
  2. 业务工具 (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
相关推荐
L、21810 小时前
CANN异构计算实践:CPU+NPU协同工作的最佳模式
网络·人工智能·pytorch·python·安全
fa_lsyk10 小时前
安装部署Claude Code及测试
人工智能
2601_9578822410 小时前
一条视频如何自动适配5大平台的技术实现
人工智能·算法·机器学习
AI小百科10 小时前
目前开源AI编辑器面临的主要挑战是什么
人工智能·开源·编辑器
TDK村田muRata10 小时前
CUS200M-12 | TDK医疗电源|直流12V 16.7A |CUS200M-12/A
服务器·人工智能·3d·机器人·无人机
csdn小瓯10 小时前
日志规范化与结构化输出:构建可观测的 AI 后端系统
人工智能
Yuk丶10 小时前
厌倦了假AI对话?用本地大模型给UE注入真智能(已开源!)
c++·人工智能·开源·ue4·游戏程序·ue4客户端开发
udc小白10 小时前
Excel实现LSTM示例
人工智能·深度学习·神经网络·机器学习·excel·lstm
完成大叔10 小时前
对话管理模式驱动的智能助手应用
人工智能