pokemon agent runtime 系列(二):一次提问的完整调用链

April 20, 2026

pokemon agent runtime 系列(二):一次提问的完整调用链

pokemon agent runtime · Runtime 2

一篇面向工程实现的调用链路拆解:前端如何发问、后端如何分流、普通模式与 Agent 模式如何走不同执行路径,以及为什么“知识库关闭但还能回答”。

系列导航:上一篇见 pokemon agent runtime 系列(一):系统设计全景图;下一篇见 pokemon agent runtime 系列(三):本地直答、缓存与 RAG 主链。本文负责回答:用户问一句之后,系统到底经历了什么。

很多聊天系统文章都会停留在一句抽象描述:前端发请求,后端调模型,再把结果流式返回。这句话没有错,但它不足以解释真实系统里最关键的工程问题:请求到底在哪一层分流?为什么有的问题不会进入知识库?为什么同样是一个输入,在普通模式和 Agent 模式下会走完全不同的路径?为什么页面上关掉知识库之后,有些问题仍然能答对?

pokemon agent 很适合用来回答这些问题。这个项目不是一个“单次 LLM 调用”的 Demo,而是一个把本地直答、缓存、RAG、知识图谱、Web Search、MCP 工具和 LangGraph Agent 调度混在同一条产品链路里的工程系统。也因此,理解它的最佳方式不是从某个单独模块开始,而是沿着一次真实提问,把整条调用链路走完。

TL;DR

问题简短结论
一次提问的前端入口在哪里?ChatView.vue -> ChatComponent.vue -> useChat.ts
普通模式和 Agent 模式在哪分叉?前端发请求时就已经分叉:/chat/ vs /chat/agent/supervisor_agent
普通模式会直接调 LLM 吗?不会,通常先经过本地直答 → 缓存 → 检索增强 → 模型生成
“知识库关闭但还能回答”说明什么?往往不是 KB 还在工作,而是命中了本地宝可梦事实直答
多种检索能力如何一起工作?Retriever 统一编排 KB / Graph / Web / MCP,并在需要时并行执行。
Agent 模式的核心价值是什么?把“下一步调用哪个能力”从后端固定逻辑,升级成 supervisor + workers 的图式调度。
前端的逐字输出是怎么实现的?后端返回 application/x-ndjson,前端按行读取并增量更新 UI。

先建立全局视角:这条链不是一条直线,而是一个分流系统

如果只看产品界面,用户动作很简单:输入一句话,点发送,然后等待回答。但从系统角度看,这个动作并不是“发出一个 query”而已,而是会同时携带一组运行时约束:

  • 是否开启知识库
  • 是否开启知识图谱
  • 是否允许联网搜索
  • 是否启用 MCP
  • 是否进入 Agent 模式
  • 当前会话历史有哪些消息
  • 当前选择了哪个知识库数据库

也就是说,用户真正发出的不是一个字符串,而是一个带状态的执行请求

FrontendChatView / ChatComponent / useChatRouterFastAPI chat_router / agent router普通模式本地直答 → 缓存 → 检索 → LLMAgent 模式supervisor → workers → finalizerLangGraph orchestration能力层 / 基础设施KB(Milvus) · Graph(Neo4j) · Web SearchMCP · ASR · Runtime Config · Feature Flags最终统一返回 NDJSON 流前端逐块消费并更新消息

从这个角度看,整条链路更像一个请求执行器,而不是一个简单的聊天接口。前端负责声明上下文与约束,后端负责根据这些约束选择不同执行路径,再把结果组织成统一流式协议回给 UI。

前端阶段:用户输入是怎样变成结构化请求的

一次请求的前端入口主要在三处:

  • web/src/views/ChatView.vue
  • web/src/components/ChatComponent.vue
  • web/src/composables/useChat.ts

其中真正把输入变成请求体的是 sendChatRequest(...)。它在发送前做了三件很关键的事:

  1. 读取当前对话历史
  2. meta 中抽取本次执行约束
  3. 根据 use_agent 选择普通模式还是 Agent 模式 endpoint

下面这段代码基本就是整个前端执行入口的缩影:

const history = messageUtils.getHistory(meta.history_round).slice(0, -1)
const requestMeta = {
  ...meta,
  db_id: dbId,
  mcp_id: meta.use_mcp ? 'default' : null
}
 
if (isAgentMode) {
  const allowedWorkers = ['rag_worker', 'stats_worker']
  if (meta.agent_allow_web !== false) allowedWorkers.push('web_worker')
  if (meta.agent_allow_graph !== false) allowedWorkers.push('graph_worker')
  if (meta.agent_allow_mcp !== false) allowedWorkers.push('mcp_worker')
  requestMeta.agent_constraints = { allowed_workers: allowedWorkers }
}
 
const endpoint = isAgentMode ? '/chat/agent/supervisor_agent' : '/chat/'

这段逻辑里有两个很重要的工程点。

第一,meta 不是 UI 状态,而是执行参数

很多系统会把“是否开启知识库”停留在前端状态层,但这个项目里,meta 会真正进入后端执行流程。它不是装饰性字段,而是驱动后端路径选择的输入。

第二,普通模式与 Agent 模式在前端就已经分叉

这点很关键。很多人会以为 Agent 是后端内部再做一层判断,但这里不是。前端发请求时就已经决定:

  • /chat/:普通聊天链路
  • /chat/agent/supervisor_agent:Agent 编排链路

也就是说,系统不是在“一个统一入口里晚分流”,而是在客户端就已经做了第一轮架构级别的路径选择。

一张图看懂前端到后端的第一次分叉

用户输入query + 当前会话上下文useChat.ts组装 history注入 meta / db_id / mcp_id必要时注入 agent_constraints选择 endpoint普通模式POST /chat/Agent 模式POST /chat/agent/supervisor_agentchat_router.py普通聊天执行器supervisor_agentLangGraph graph entry

普通模式主链:不是直接调模型,而是先做三层拦截

普通模式的后端入口在 server/routers/chat_router.py。这里最值得强调的一点是:它并不会在收到请求后立刻调模型。相反,它会先经历三层拦截:

  1. 本地事实直答
  2. 语义缓存
  3. 检索增强

只有这三层都没有直接结束请求,才会进入模型生成。

第一层:本地事实直答

这一层非常像一个 domain-specific fast path。对于明显的宝可梦事实问题,系统会优先尝试走本地数据,而不是启动更昂贵的检索和模型调用流程。

def need_retrieve(meta: dict[str, Any]) -> bool:
    return meta.get("use_web") or meta.get("use_graph") or meta.get("db_id") or meta.get("mcp_id")
 
# 0. Local deterministic Pokédex shortcut
if decision.intent == Intent.POKEDEX_FACTS and (decision.confidence >= 0.8 or decision.needs_clarification):
    content = format_basic_facts(rec) + "\n\n" + format_evolution(rec, data=data)
    refs = {
        "model_name": "local_dataset",
        "knowledge_base": {
            "results": [],
            "message": "Answer来自本地数据集(无 LLM 调用)",
        },
    }
    yield make_chunk(meta, content=content, status="loading")
    yield make_chunk(meta, status="finished", refs=refs)
    return

这也是为什么在这个项目里,知识库关闭但还能回答 往往并不说明 KB 失效了,而只是说明:这个问题根本没有走到 KB 那一层

第二层:语义缓存

如果当前请求没有历史消息,并且也不需要检索,系统会尝试命中缓存:

safe_cache = (not history) and (not need_retrieve(meta))
if safe_cache:
    cached_response = cache.get(query, meta=cache_meta)
    if cached_response:
        yield make_chunk(meta, content=cached_response, status="finished")
        return

这里的关键不是“有缓存”本身,而是缓存使用条件被故意限制得很保守。它只在单轮、非检索场景启用,避免跨轮上下文污染,也避免把一个本应依赖外部状态的回答缓存成“静态答案”。

第三层:检索增强

只有在 need_retrieve(meta) 为真时,普通模式才会进入检索阶段。换句话说,检索不是默认行为,而是显式由请求参数触发

if meta and need_retrieve(meta):
    yield make_chunk(meta, status="searching")
    retriever = get_retriever()
    modified_query, refs = await to_thread(
        retriever,
        modified_query,
        history,
        meta,
    )
    yield make_chunk(meta, status="generating")

从执行顺序看,这一层的作用不是直接回答,而是先把问题改造成一个更适合后续生成的增强 query。这意味着普通模式下的 RAG 更像一个prompt augmentation pipeline,而不是独立的回答器。

Retriever:多源检索不是并排堆功能,而是统一编排

多源检索的核心在 src/knowledge/core/retriever.py。这个模块最值得注意的不是“它能查什么”,而是“它如何决定怎么查”。

下面这段代码很能说明它的定位:

def retrieval(self, query: str, history: list[dict[str, Any]], meta: dict[str, Any]) -> dict[str, Any]:
    refs = {"query": query, "history": history, "meta": meta}
 
    requested = sum(
        1
        for v in (
            bool(meta.get("db_id")),
            bool(meta.get("use_graph")),
            bool(meta.get("use_web")),
            bool(meta.get("mcp_id")),
        )
        if v
    )
 
    if requested >= 2:
        with ThreadPoolExecutor(max_workers=4) as ex:
            fut_kb = ex.submit(self.query_knowledgebase, query, history, refs)
            fut_graph = ex.submit(self.query_graph, query, history, refs)
            fut_web = ex.submit(self.query_web, query, history, refs)
            fut_mcp = ex.submit(self.query_mysql_mcp, query, history, refs)

这里至少有三层值得关注的设计。

第一,它统一管理多种证据源

在很多原型系统里,知识库、图谱、Web Search 往往是散落在不同接口里的。但这里不同,它们都收敛到同一个 Retriever 里。这样做的好处是:

  • 更容易统一管理 meta
  • 更容易统一聚合引用结果 refs
  • 更容易控制并发与延迟

第二,它会在必要时并行执行

当一个请求同时打开多个能力源时,系统不会串行一个个查,而是并行发起多个检索分支,把总延迟压到更接近 max(step_latency) 而不是 sum(step_latency)

第三,它最终服务的是生成阶段,而不是直接替代生成阶段

检索结束后,Retriever.construct_query(...) 会把图谱回答、知识库片段、Web 搜索结果、MCP 结果拼成增强上下文,再交给模型生成。这意味着它在架构中的位置是证据聚合器,而不是最终回答器。

用一张表看懂普通模式的分流条件

阶段触发条件主要文件典型结果
本地直答命中明确宝可梦事实意图server/routers/chat_router.py直接从本地数据返回,model_name = local_dataset
语义缓存单轮 + 非检索请求server/routers/chat_router.py直接返回缓存内容
检索增强use_web / use_graph / db_id / mcp_id 任一为真src/knowledge/core/retriever.py聚合外部证据并改写 query
模型生成前三层未直接结束请求server/routers/chat_router.py进入 LLM 流式生成

这张表能解释一件很常见、但也最容易误判的事情:在这个系统里,“回答来源”不是一个二元判断,不是只有“模型”或“知识库”两种,而是至少有四种可能的路径。

Agent 模式:从固定分流升级成图式调度

如果用户打开 Agent mode,系统就不再走 chat_router.py 的普通模式主链,而是进入 supervisor_agentsrc/graph/workflow.py 所定义的 LangGraph 工作流。

这里的核心变化不是“模型更聪明了”,而是流程控制权发生了变化

在普通模式下,后端代码明确写死了执行顺序:本地直答、缓存、检索、生成。模型只在最后一段出现。

在 Agent 模式下,系统进入的是一种 supervisor + workers 的调度结构:

  • supervisor 决定下一步去哪个 worker
  • rag_worker 负责 RAG / KB 路径
  • graph_worker 负责图谱路径
  • web_worker 负责联网搜索
  • mcp_worker 负责 MCP 工具能力
  • stats_worker 负责统计或结构化辅助能力
  • finalizer 负责最终收束输出

这里特别值得补一句:Agent 模式虽然包含 rag_worker,但Agent 模式本身并不等于 RAG。更准确地说,Agent 是一层编排框架,而 RAG 只是其中一种可被调度的能力。一个问题如果被派给 graph_workerweb_workerstats_worker,它走的就不是 RAG 路径,而是图谱、联网搜索或确定性事实/统计路径。

几个最常被追问的概念澄清

问题更准确的理解
supervisor 是不是一个“裸 LLM”?不是。它首先走规则与确定性 fast path;只有规则不够时,才用带 handoff tools 的 LLM 做兜底路由。
supervisor_agentsupervisor 是一回事吗?也不是。supervisor_agent 更像图执行入口和 runtime adapter;真正做路由决策的是图里的 supervisor 节点。
graph_workerrag_worker 一样吗?不一样。graph_worker 面向图结构关系查询;rag_worker 面向文档/向量检索后的生成。两者都可能有本地事实兜底,但依赖的数据形态完全不同。
stats_worker 是做什么的?它既是一个常见的规则直达目标节点,也常常在 worker 内部先做确定性直答,例如属性克制、图鉴事实这类问题。
Agent 模式里的 rag_worker 和普通模式 RAG 一样吗?不完全一样。普通模式更像“检索增强主链”;Agent 模式里的 rag_worker 是一个被调度的独立 worker,而且通常更像一条增强版 RAG 子流水线。

如果用一句很口语化的话概括这一组关系,那就是:

supervisor 决定“谁来干”,worker 负责“怎么干”,finalizer 负责“怎么把内部结果说成人能直接读的答案”。

workflow.py 里最关键的部分是下面这段:

def _supervisor_route(state: AgentState) -> list[Send] | str:
    next_target = state.get("next", "FINISH")
 
    if next_target == "__PARALLEL__":
        parallel_workers = state.get("parallel_workers", [])
        if parallel_workers:
            return [Send(worker, state) for worker in parallel_workers]
        return END
 
    if next_target == "FINISH":
        return "finalizer"
 
    return next_target

这段代码直接揭示了 Agent 模式最重要的能力:同一个问题可以被拆给多个 worker 并行执行。这也是普通模式和 Agent 模式最大的结构差异。

但也别误解成“整个 Agent 都在并行跑”

并行能力在这个项目里是局部存在的,而不是所有步骤都并行。

  • 在 workflow 层,supervisor 返回 __PARALLEL__ 时,多个 worker 可以通过 Send(...) 并行执行
  • rag_worker 内部,只有“多个子问题的检索召回”这一步会并行
  • 但像 db_id 分支判断、是否做 query decomposition、是否启用 HyDE、最后把上下文交给 LLM 生成答案,这些步骤本身仍然是串行控制流程

因此,这套系统更准确的描述是:串行主流程里嵌入局部并行阶段,而不是一条从头到尾全并行的流水线。

普通模式与 Agent 模式,到底差在哪?

维度普通模式Agent 模式
入口/chat//chat/agent/supervisor_agent
流程控制权主要在后端代码supervisor 根据状态动态调度
检索方式通过 Retriever 聚合证据通过多个 worker 调度能力
典型结构串行决策链图式编排 / 支持并行
优势稳定、清晰、低成本灵活、可扩展、可做多能力组合
风险对复杂多步问题不够灵活更难约束,更依赖调度策略

从工程角度看,这不是“哪个更高级”的问题,而是“哪个更适合当前问题复杂度”的问题。

最后一段链:为什么前端能逐字显示答案

无论普通模式还是 Agent 模式,最终都会回到同一个输出协议:NDJSON 流。

前端侧的读取逻辑非常直接:

export async function readNdjsonStream(response, onJson, { onParseError } = {}) {
  const reader = response.body.getReader()
  const decoder = new TextDecoder('utf-8')
  let buffer = ''
 
  while (true) {
    const { done, value } = await reader.read()
    if (done) break
 
    buffer += decoder.decode(value, { stream: true })
    const lines = buffer.split('\n')
    buffer = lines.pop() || ''
 
    for (const rawLine of lines) {
      const line = rawLine.trim()
      if (!line) continue
      await onJson(JSON.parse(line))
    }
  }
}

后端返回的是 application/x-ndjson,也就是“每一行一个 JSON 对象”。前端并不等整个响应完成,而是边读边切分、边解析边更新消息。

这背后的好处很明显:

  • 不需要引入 WebSocket
  • 与现有 HTTP 请求链兼容
  • 支持统一的 status / refs / meta / response 协议

也就是说,用户看到的“逐字输出”,本质上不是某种神秘的流式魔法,而是一个很朴素、但很有效的 NDJSON 增量消费模式。

一张时序图:从用户输入到流式渲染结束

User / UIuseChat.tsBackendCapability Layer输入问题并发送组装 history + meta路由与分流本地直答 / RAG / Graph /Web / MCP / Agent Workers输出 NDJSON逐块解析并更新消息

为什么这条调用链值得单独写一篇

因为它回答了很多 AI 工程系统中最常被问到、但又最容易被讲得模糊的问题:

  • 一个聊天系统如何把 UI 状态转成后端执行约束?
  • 普通模式和 Agent 模式该如何共存?
  • 本地知识、检索增强、工具调用和模型生成之间,应该如何分层?
  • 前端“看起来只是一次发送”,背后到底触发了多少层执行逻辑?

pokemon agent 的价值不只在于它做了一个宝可梦问答系统,而在于它把很多现代 AI 应用会遇到的工程问题,真实地压缩进了一条用户可感知的产品链里:同一套 UI,背后既能走简单本地直答,也能走多源检索增强,还能切到 supervisor + workers 的 Agent 编排

从系统设计角度看,这种“单产品,多执行路径”的结构,比单独看某个组件更有学习价值。