从输入到 LLM 循环:智能体的心脏

本文档带你追踪用户输入文本后的完整数据流,从 TUI 编辑器到 LLM 调用,再到工具执行的循环。以 Pi v0.80.10 源码为基准。

全景图

用户在编辑器中输入 "帮我重构这个函数" 并按 Enter


┌─────────────────────────────────────────────────────────┐
│ 阶段 A:TUI → AgentSession                               │
│ InteractiveMode.run()                                    │
│   getUserInput() → "帮我重构这个函数"                     │
│   session.prompt(userInput)                              │
└─────────────────────────┬───────────────────────────────┘


┌─────────────────────────────────────────────────────────┐
│ 阶段 B:消息预处理 (AgentSession.prompt)                  │
│   1. 检查是否斜杠命令 → 否                                │
│   2. 扩展事件触发(扩展可拦截/修改输入)                   │
│   3. 展开 skill / prompt template                         │
│   4. 通过 ModelRuntime 准备模型和认证                     │
│   5. 检查是否需要压缩(compaction)                       │
│   6. 构建 AgentMessage[]                                  │
│   7. _runAgentPrompt(messages)                            │
└─────────────────────────┬───────────────────────────────┘


┌─────────────────────────────────────────────────────────┐
│ 阶段 C:Agent 启动 (Agent.prompt)                         │
│   normalizePromptInput() → AgentMessage[]                │
│   runPromptMessages(messages)                            │
│     → runWithLifecycle() → runAgentLoop()                │
└─────────────────────────┬───────────────────────────────┘


┌─────────────────────────────────────────────────────────┐
│ 阶段 D:★ Agent Loop(核心循环)                           │
│                                                          │
│   while (true) { // 外层:follow-up 循环                  │
│     while (hasMoreToolCalls || pendingMessages) {        │
│                                                  │
│       ① 注入 pending messages(steer/follow-up)         │
│       ② streamAssistantResponse()                        │
│          → 通过 Models 运行时调用 LLM                    │
│       ③ 检查 stopReason                                  │
│          → error/aborted → 退出                          │
│       ④ 提取 tool calls                                  │
│          → 有 → executeToolCalls()                       │
│          → 无 → hasMoreToolCalls = false                 │
│     }                                                    │
│                                                          │
│     检查 follow-up messages                              │
│     → 有 → 设为 pending → continue 外层循环              │
│     → 无 → break,退出                                   │
│   }                                                      │
└─────────────────────────────────────────────────────────┘

阶段 A:TUI 传递输入

文件packages/coding-agent/src/modes/interactive/interactive-mode.ts

async run(): Promise<void> {
  await this.init();

  // 主循环
  while (true) {
    const userInput = await this.getUserInput(); // ← 挂起等待输入
    try {
      await this.session.prompt(userInput);      // ← 输入到达
    } catch (error) {
      this.showError(errorMessage);
    }
  }
}

getUserInput() 用 Promise 挂起,编辑器提交时 resolve。

阶段 B:消息预处理

文件packages/coding-agent/src/core/agent-session.ts

这是输入到 LLM 之间最重要的关卡prompt() 方法对输入做多层处理:

B1. 斜杠命令

if (expandPromptTemplates && text.startsWith('/')) {
  const handled = await this._tryExecuteExtensionCommand(text);
  if (handled) return;
}

/ 开头的输入先尝试执行扩展命令;若未被处理,再作为普通消息发送。

B2. 扩展事件拦截

if (this._extensionRunner.hasHandlers('input')) {
  const inputResult = await this._extensionRunner.emitInput(
    currentText,
    currentImages,
    options?.source ?? 'interactive',
    this.isStreaming ? options?.streamingBehavior : undefined,
  );
  if (inputResult.action === 'handled') return;
  if (inputResult.action === 'transform') {
    currentText = inputResult.text;
    currentImages = inputResult.images ?? currentImages;
  }
}

扩展可返回三种动作:

动作效果
pass跳过,继续下一个扩展或默认流程
handled扩展已处理,不再发送给 LLM
transform修改输入后继续处理

B3. Skill 和 Prompt Template 展开

if (expandPromptTemplates) {
  expandedText = this._expandSkillCommand(expandedText);
  expandedText = expandPromptTemplate(expandedText, [...this.promptTemplates]);
}

B4. 流式处理检查

if (this.isStreaming) {
  if (options.streamingBehavior === 'followUp') {
    await this._queueFollowUp(expandedText, currentImages);
  } else {
    await this._queueSteer(expandedText, currentImages);
  }
  return;
}

SteerFollow-up 的区别:

行为触发条件效果
SteerAgent 正在工作中输入当前工具执行完成后、下一次 LLM 调用前注入消息
Follow-upAgent 正在工作中输入Agent 完全完成后作为新问题排队

B5. 验证模型与认证

// 当前 coding-agent 由 ModelRuntime 统一提供模型和认证能力。
const model = this.modelRuntime.getModel(provider, modelId);
if (!model) throw new Error(`Unknown model: ${provider}/${modelId}`);

ModelRuntime 会把运行时凭证、持久化凭证、Provider 配置和 pi-ai 的认证策略组合起来:

  1. runtime 覆盖(--api-key
  2. auth.json 中保存的 API key / OAuth token
  3. Provider 配置中的 apiKey(如 auth.jsonenv 覆盖)
  4. 环境变量(通过 pi-ai envApiKeyAuth

详见 项目信任与认证体系

B6. 检查是否需要压缩

const lastAssistant = this._findLastAssistantMessage();
if (lastAssistant && (await this._checkCompaction(lastAssistant, false))) {
  await this.agent.continue();
  while (await this._handlePostAgentRun()) {
    await this.agent.continue();
  }
}

压缩的触发时机包括:

  • 手动:用户输入 /compact
  • 阈值:上下文 token 数超过设置阈值
  • 溢出:LLM 返回 context overflow 错误

压缩事件还会携带 reasonmanual / threshold / overflow)和 willRetry,让扩展区分手动压缩、阈值压缩与溢出重试。详见 上下文压缩与会话分支

B7. 构建消息并发送

messages = [
  {
    role: 'user',
    content: [{ type: 'text', text: expandedText }, ...(currentImages ?? [])],
    timestamp: Date.now(),
  },
];

await this._runAgentPrompt(messages);

阶段 C:Agent 启动

文件packages/coding-agent/src/core/agent-session.ts _runAgentPromptpackages/agent/src/agent.tspackages/agent/src/agent-loop.ts

private async _runAgentPrompt(messages: AgentMessage[]): Promise<void> {
  try {
    await this.agent.prompt(messages);
    while (await this._handlePostAgentRun()) {
      await this.agent.continue();
    }
  } finally {
    this._flushPendingBashMessages();
  }
}

Agent.prompt() 将输入归一化为 AgentMessage[],然后调用 runPromptMessages()

private async runPromptMessages(messages: AgentMessage[]): Promise<void> {
  await this.runWithLifecycle(async (signal) => {
    await runAgentLoop(
      messages,
      this.createContextSnapshot(),
      this.createLoopConfig(),
      (event) => this.processEvents(event),
      signal,
      this.streamFn,
    );
  });
}

关键:createLoopConfig() 中配置的 streamFn 是调用 pi-ai 的入口。在 coding-agent 中,它通常闭包捕获 ModelRuntime,再调用 modelRuntime.streamSimple();Agent Loop 不需要知道凭证存在哪里。

阶段 D:Agent Loop 核心循环

文件packages/agent/src/agent-loop.ts

async function runLoop(
  initialContext: AgentContext,
  newMessages: AgentMessage[],
  initialConfig: AgentLoopConfig,
  signal: AbortSignal | undefined,
  emit: AgentEventSink,
  streamFn?: StreamFn,
): Promise<void> {
  let currentContext = initialContext;
  let pendingMessages: AgentMessage[] = (await config.getSteeringMessages?.()) || [];

  while (true) {
    let hasMoreToolCalls = true;

    while (hasMoreToolCalls || pendingMessages.length > 0) {
      if (!firstTurn) await emit({ type: 'turn_start' });

      // 注入 pending messages
      if (pendingMessages.length > 0) {
        for (const message of pendingMessages) {
          currentContext.messages.push(message);
          newMessages.push(message);
        }
        pendingMessages = [];
      }

      // ① 调用 LLM
      const message = await streamAssistantResponse(currentContext, config, signal, emit, streamFn);
      newMessages.push(message);

      if (message.stopReason === 'error' || message.stopReason === 'aborted') {
        await emit({ type: 'turn_end', message, toolResults: [] });
        await emit({ type: 'agent_end', messages: newMessages });
        return;
      }

      // ② 提取 tool calls
      const toolCalls = message.content.filter((c) => c.type === 'toolCall');
      hasMoreToolCalls = false;

      if (toolCalls.length > 0) {
        const executedToolBatch = await executeToolCalls(currentContext, message, config, signal, emit);
        hasMoreToolCalls = !executedToolBatch.terminate;
        for (const result of executedToolBatch.messages) {
          currentContext.messages.push(result);
          newMessages.push(result);
        }
      }

      await emit({ type: 'turn_end', message, toolResults: executedToolBatch.messages });
    }

    // ③ 检查 follow-up
    const followUpMessages = (await config.getFollowUpMessages?.()) || [];
    if (followUpMessages.length > 0) {
      pendingMessages = followUpMessages;
      continue;
    }

    break;
  }

  await emit({ type: 'agent_end', messages: newMessages });
}

streamAssistantResponse 详解

async function streamAssistantResponse(
  context: AgentContext,
  config: AgentLoopConfig,
  signal: AbortSignal | undefined,
  emit: AgentEventSink,
  streamFn?: StreamFn,
): Promise<AssistantMessage> {
  const llmMessages = await config.convertToLlm(messages);
  const llmContext: Context = {
    systemPrompt: context.systemPrompt,
    messages: llmMessages,
    tools: context.tools,
  };

  // v0.80:streamFn 闭包通过 ModelRuntime 解析认证并调用 Provider
  const response = await streamFunction(config.model, llmContext, { signal });

  for await (const event of response) {
    switch (event.type) {
      case 'start':
      case 'text_delta':
      case 'thinking_delta':
      case 'toolcall_delta':
        partialMessage = event.partial;
        context.messages[context.messages.length - 1] = partialMessage;
        await emit({ type: 'message_update', assistantMessageEvent: event, message: { ...partialMessage } });
        break;
      case 'done':
      case 'error':
      // ...
    }
  }

  return partialMessage!;
}

streamFn 不负责设计认证优先级。它接收模型、上下文和请求选项,应用层闭包再把请求交给 ModelRuntime;ModelRuntime 最终调用 pi-ai Provider 的 streamSimple()

executeToolCalls 详解

LLM 返回 ToolCall { name: "read", arguments: { path: "package.json" } }


executeToolCalls()

  ├─ 1. 查找工具定义
  ├─ 2. validateToolArguments() — TypeBox schema 校验
  ├─ 3. 发出 tool_call_start 事件
  ├─ 4. 执行工具(handler)
  │     read 工具:fs.readFileSync(path, 'utf8')
  ├─ 5. 发出 tool_result 事件
  └─ 6. 返回 ToolResultMessage

循环终止条件

条件stopReason行为
LLM 正常回复完成"stop"内层循环退出
LLM 调用工具"toolUse"继续内层循环
输出超过最大 token"length"内层循环退出
发生错误"error"直接 return
用户中断(Ctrl+C)"aborted"直接 return
用户输入 steer注入后继续
用户输入 follow-up外层循环继续

关键概念总结

概念解释代码位置
AgentSession业务逻辑中枢,处理消息预处理agent-session.ts
Agent智能体运行时,管理状态和生命周期agent.ts
runAgentLoop★ 核心循环,LLM → 工具 → 循环agent-loop.ts
streamAssistantResponseLLM 调用入口,处理流式事件agent-loop.ts
executeToolCalls工具执行,验证 → 执行 → 返回结果agent-loop.ts
SteerAgent 工作中注入消息,当前工具完成后生效agent-session.ts
Follow-upAgent 完成后排队的新消息agent-session.ts
ModelRuntime模型、Provider 与认证协调model-runtime.ts
事件驱动所有状态变化通过事件通知 TUIemit() 调用

下一步

核心架构与设计哲学 — Provider 抽象、事件流、TUI 差分渲染、扩展系统