通用 Agent 对话循环设计文档

名称

通用 Agent 对话循环设计文档

分类

prompt

路径

prompt/agent-design-notes/step5-final-design.md

描述

基于 Claude Code 源码分析提炼,适用于构建具备自主工具调用能力的对话 Agent

通用 Agent 对话循环设计文档

基于 Claude Code 源码分析提炼,适用于构建具备自主工具调用能力的对话 Agent


一、整体架构

┌─────────────────────────────────────────────────────────────────┐
│                         用户 / 外部调用方                          │
└──────────────────────────┬──────────────────────────────────────┘
                           │ 提交消息 (prompt + 结构化数据 + 意图)
┌──────────────────────────▼──────────────────────────────────────┐
│                      AgentSession (会话层)                        │
│                                                                   │
│  持有: 消息历史 | 中断控制 | 用量统计 | 权限拒绝记录 | 文件状态缓存    │
│  职责: 会话生命周期管理,跨多轮 submitMessage() 保持状态             │
└──────────────────────────┬──────────────────────────────────────┘
                           │ query(messages, systemPrompt, tools)
┌──────────────────────────▼──────────────────────────────────────┐
│                      AgentLoop (循环层)                            │
│                                                                   │
│  while(true) {                                                    │
│    1. 预处理上下文 (压缩/裁剪)                                       │
│    2. 调用 LLM → 获得助手响应                                        │
│    3. 解析 tool_use blocks                                         │
│    4. 执行工具 → 获得 tool_result                                   │
│    5. 将结果拼回消息历史                                             │
│    6. 判断退出 or 继续                                              │
│  }                                                                │
└──────────────────────────┬──────────────────────────────────────┘
                           │
        ┌──────────────────┼──────────────────┐
        │                  │                  │
┌───────▼──────┐  ┌────────▼───────┐  ┌──────▼────────┐
│  Prompt 构建  │  │   工具编排执行   │  │  上下文管理    │
│  (三层结构)   │  │  (权限+并发)    │  │  (压缩+裁剪)  │
└──────────────┘  └────────────────┘  └───────────────┘

二、核心数据流

2.1 输入处理流程

用户输入 (文本 + 结构化数据 + 意图)
  │
  ├─ 预处理 processUserInput()
  │   ├─ 识别并执行 slash 命令 (/clear, /compact 等)
  │   ├─ 处理图片、文件附件
  │   └─ 检查是否需要调用 LLM (shouldQuery)
  │
  ├─ 构建 systemPrompt
  │   ├─ [静态] 角色定义 + 行为规范 + 工具使用说明
  │   ├─ [动态] 运行时上下文注入 (环境信息、记忆文件)
  │   └─ [追加] 调用方自定义指令 (appendSystemPrompt)
  │
  └─ 进入 AgentLoop

2.2 单轮 AgentLoop 数据流

messages (当前历史) + systemPrompt
  │
  ├─ [预处理] 上下文压缩 (如需要)
  │
  ├─ LLM API 调用
  │   └─ 流式返回 AssistantMessage
  │       ├─ TextBlock         → 直接输出给用户
  │       ├─ ThinkingBlock     → 内部推理 (可选展示)
  │       └─ ToolUseBlock[]    → 需要执行的工具调用列表
  │
  ├─ 工具执行 (若有 ToolUseBlock)
  │   ├─ 权限检查 canUseTool()
  │   ├─ 并发/串行调度
  │   └─ 收集 tool_result[]
  │
  ├─ 拼接: messages += [AssistantMessage, UserMessage(tool_results)]
  │
  └─ 判断:
      ├─ 无 ToolUseBlock → 退出循环,返回最终回复
      └─ 有 ToolUseBlock → 继续下一轮

三、Prompt 工程设计

3.1 三层 Prompt 结构

Layer 1 -- systemPrompt (LLM API system 字段)
  ┌──────────────────────────────────────────┐
  │ [可缓存: 静态内容]                         │
  │  角色定位 + 行为规范 + 安全约束             │
  │  工具使用指南 + 特定领域知识                │
  │  ─────────── 动态分界线 ──────────────    │
  │ [动态内容]                                 │
  │  MCP 工具说明 + 记忆文件 + 用户偏好         │
  └──────────────────────────────────────────┘

Layer 2 -- userContext (注入第一条 user 消息前)
  ┌──────────────────────────────────────────┐
  │  当前时间 + 工作目录 + 环境变量             │
  │  运行时状态快照 (git status 等)             │
  │  用户提供的结构化数据 (本次调用专用)         │
  └──────────────────────────────────────────┘

Layer 3 -- appendSystemPrompt (追加在末尾)
  ┌──────────────────────────────────────────┐
  │  调用方动态追加的特定指令                   │
  │  (不覆盖主 prompt,仅补充)                 │
  └──────────────────────────────────────────┘

3.2 面向结构化数据的 Prompt 模板

当用户提供结构化数据时,推荐在 userContext 层注入:

<userContext>
  <currentTime>2026-04-21T10:30:00+08:00</currentTime>
  <userIntent>用户意图描述</userIntent>
  <structuredData>
    <!-- 用户提供的结构化数据 (JSON / 表格 / 列表等) -->
    {
      "field1": "value1",
      "records": [...]
    }
  </structuredData>
  <constraints>
    <!-- 本次任务的约束条件 -->
  </constraints>
</userContext>

3.3 系统提示词 Section 组织

# 角色定位
你是一个...Agent,帮助用户完成...任务。

# 行为规范
- 在给出最终回答前,先分析用户提供的数据
- 对于不确定的信息,主动询问而非猜测
- ...

# 工具使用指南
当需要...时,使用 XXXTool 而非直接在回复中处理。
...

# 数据处理规则
用户会提供结构化数据,处理时应注意:
- ...

────── 动态内容分界 ──────

# 当前上下文
(动态注入: 会话状态、记忆内容等)

四、工具系统设计

4.1 最小工具定义

type Tool = {
  name: string           // 唯一标识,LLM 通过此名调用
  description: string    // 告诉 LLM 此工具的用途和使用时机
  inputSchema: ZodSchema // 参数定义 (自动生成 JSON Schema 给 LLM)
  
  call(input, context): AsyncGenerator<ProgressEvent | ToolResult>
  
  isReadOnly(input): boolean  // true 可并发,false 串行
}

4.2 权限控制接口

type PermissionResult =
  | { behavior: 'allow' }
  | { behavior: 'deny'; reason: string }
  | { behavior: 'ask'; prompt: string; onDecision: (allowed: boolean) => void }

async function canUseTool(tool, input, context): Promise<PermissionResult>

4.3 工具执行策略

LLM 返回多个 tool_use:
  │
  partition(toolUses, isReadOnly)
  │
  ├─ 只读批次 → Promise.all 并发执行
  └─ 写入批次 → 顺序串行执行

4.4 错误处理原则

工具执行失败时不抛异常,返回 is_error: true 的 tool_result:

{
  "type": "tool_result",
  "tool_use_id": "xxx",
  "is_error": true,
  "content": "执行失败原因描述,LLM 可据此调整策略"
}

五、上下文管理策略

5.1 压缩触发阈值设计

Token 占用率:
  < 60%   → 正常运行
  60-80%  → 显示警告提示
  80-95%  → 触发自动压缩
  > 95%   → 硬性阻止,必须先压缩

5.2 压缩优先级

优先使用低成本方案:
  1. Snip 裁剪 (零成本: 直接删除旧消息段)
  2. 微压缩 (低成本: 只压缩过大的单条工具结果)
  3. 全量摘要 (高成本: 调用 LLM 生成对话摘要)

5.3 compact_boundary 模式

历史消息 [m1...mN] + compact_boundary + 摘要 + [新消息]

优点:
  - LLM 只看摘要和新消息,节省 token
  - UI 仍可展示完整历史 (REPL scrollback)
  - 会话可从边界恢复 (/resume)

六、完整实现伪代码

class AgentSession {
  private messages: Message[] = []
  private abortController = new AbortController()

  async *submitMessage(userPrompt: string, structuredData?: unknown) {
    // 1. 构建系统提示词
    const systemPrompt = buildSystemPrompt({
      roleDefinition: ROLE_PROMPT,
      toolGuide: TOOL_GUIDE_PROMPT,
      memoryContent: await loadMemory(),
    })

    // 2. 构建用户上下文
    const userContext = {
      currentTime: new Date().toISOString(),
      structuredData: structuredData,
      userIntent: extractIntent(userPrompt),
    }

    // 3. 处理用户输入
    const userMessage = createUserMessage(userPrompt, userContext)
    this.messages.push(userMessage)

    // 4. 进入 Agent 循环
    yield* this.agentLoop(systemPrompt)
  }

  private async *agentLoop(systemPrompt: string) {
    let turnCount = 0
    const MAX_TURNS = 20

    while (true) {
      // 上下文预处理
      const messagesForLLM = await this.preprocessContext(this.messages)

      // 调用 LLM
      const assistantMsg = await callLLM({
        system: systemPrompt,
        messages: messagesForLLM,
        tools: this.tools,
        signal: this.abortController.signal,
      })

      this.messages.push(assistantMsg)
      yield assistantMsg  // 流式输出给调用方

      // 解析工具调用
      const toolUseBlocks = extractToolUseBlocks(assistantMsg)

      if (toolUseBlocks.length === 0) {
        // 无工具调用 → 自然结束
        yield { type: 'result', subtype: 'success' }
        return
      }

      // 执行工具
      const toolResults = await this.executeTools(toolUseBlocks)
      const toolResultMsg = createUserMessage(toolResults)
      this.messages.push(toolResultMsg)
      yield toolResultMsg

      // 检查退出条件
      turnCount++
      if (turnCount >= MAX_TURNS) {
        yield { type: 'result', subtype: 'error_max_turns' }
        return
      }
    }
  }

  private async executeTools(toolUseBlocks: ToolUseBlock[]) {
    const results = []
    const { readOnly, writeOps } = partition(toolUseBlocks, isReadOnly)

    // 只读工具并发
    const readResults = await Promise.all(
      readOnly.map(block => this.executeOneTool(block))
    )
    results.push(...readResults)

    // 写入工具串行
    for (const block of writeOps) {
      results.push(await this.executeOneTool(block))
    }

    return results
  }

  private async executeOneTool(toolUseBlock: ToolUseBlock) {
    const tool = findTool(toolUseBlock.name, this.tools)
    const permission = await canUseTool(tool, toolUseBlock.input)

    if (permission.behavior === 'deny') {
      return { tool_use_id: toolUseBlock.id, is_error: true, content: permission.reason }
    }

    try {
      const result = await tool.call(toolUseBlock.input)
      return { tool_use_id: toolUseBlock.id, is_error: false, content: result }
    } catch (err) {
      return { tool_use_id: toolUseBlock.id, is_error: true, content: String(err) }
    }
  }
}

七、面向本次需求的设计建议

场景: 用户提供结构化数据 + 意图,Agent 完成对话任务

推荐实现方案

1. 数据注入策略

将结构化数据通过 userContext XML 标签注入,而非直接放入用户消息主体:

systemPrompt 声明如何使用 <structuredData> 标签
userContext 注入实际数据
用户消息只包含意图描述

2. 意图解析工具

为 Agent 提供专用工具处理结构化数据分析:

  • QueryDataTool: 在结构化数据上执行查询
  • SummarizeDataTool: 对数据进行统计摘要
  • ValidateDataTool: 验证数据格式与业务规则

3. 对话状态管理

对于多轮对话场景,推荐维护一个轻量级 session 状态:

type SessionState = {
  structuredData: unknown        // 本次提供的数据
  userIntent: string             // 解析出的用户意图
  completedSteps: string[]       // 已完成的处理步骤
  pendingQuestions: string[]     // 待用户确认的问题
}

4. Prompt 工程要点

系统提示词必须包含:
  ✅ 说明何时直接回答 vs 何时使用工具
  ✅ 说明如何解读 <structuredData> 标签
  ✅ 说明不确定时应 AskUser 而非猜测
  ✅ 说明输出格式要求 (markdown / JSON / 自然语言)
  ✅ 说明任务完成的判断标准

5. 最简可用架构

用户调用: session.submitMessage(intent, structuredData)
            ↓
        systemPrompt (意图理解 + 数据处理规范)
            ↓
        userContext (注入数据 + 运行时环境)
            ↓
        AgentLoop (LLM 推理 + 工具执行)
            ↓
        流式输出 (进度 + 最终回复)

八、关键设计决策总结

决策 Claude Code 做法 通用建议
消息历史 全量持久化,压缩时保留边界 维护完整历史,LLM 只看压缩视图
工具权限 前置检查,支持交互授权 最小权限原则,危险操作需确认
错误处理 工具错误作为 tool_result 返回 让 LLM 看到错误并自主恢复
并发策略 只读并发,写入串行 按副作用判断,宁可保守串行
上下文管理 多层压缩,compact_boundary 设计恢复点,分层处理不同粒度
Prompt 结构 Section 化 + 静态/动态分离 关注点分离,充分利用缓存
子 Agent AgentTool fork + 共享 cache 复杂任务分解为子 Agent
流式输出 AsyncGenerator 全链路 渐进式输出,早期返回进度事件