Skip to content

从 pi-mono 源码看 Agent 架构的真实取舍——容忍探索 vs 确定性收敛

MasakiMu319

一个更有用的分类框架

讨论 Agent 架构时,常见的切入角度是”个人 vs 商业”、“轻量 vs 重量”、“极简 vs 复杂”。这些直觉上有道理,但在分析具体设计决策时缺乏解释力——同一个架构可以既适合某些个人场景,又适合某些商业场景,反之亦然。

更有区分度的维度是:场景对结果的要求

这两种场景对 Agent 的行动空间、反馈机制、终止条件的要求完全不同。用这个框架去审视一个具体的 Agent 实现,能得到比”极简 vs 复杂”更有操作性的判断。

本文以 pi-mono(一个近期关注度较高的 coding agent 工具包)的源码为对象,做一次基于事实的架构分析,并横向对比 OpenAI Agents SDK、Kimi-CLI 和 Codex 的工程实践。


pi-mono 到底做了什么——源码实证

网上有不少文章把 pi-mono 的设计包装成某种”哲学”,用大量隐喻来描述其架构。这里跳过隐喻,直接看代码。

核心循环就是标准 ReAct

pi-mono 的 agent 循环在 packages/agent/src/agent-loop.tsrunLoop 函数中。先看结构骨架:

async function runLoop(currentContext, newMessages, config, signal, stream, streamFn) {
  let pendingMessages = (await config.getSteeringMessages?.()) || [];

  // 外层循环:处理 follow-up 消息
  while (true) {
    let hasMoreToolCalls = true;
    let steeringAfterTools = null;

    // 内层循环:处理 tool calls 和 steering
    while (hasMoreToolCalls || pendingMessages.length > 0) {
      // 1. 注入 pending 消息到上下文
      if (pendingMessages.length > 0) {
        for (const message of pendingMessages) {
          currentContext.messages.push(message);
          newMessages.push(message);
        }
        pendingMessages = [];
      }

      // 2. 调用 LLM
      const message = await streamAssistantResponse(currentContext, config, signal, stream, streamFn);

      // 3. 检查 toolCalls
      const toolCalls = message.content.filter((c) => c.type === "toolCall");
      hasMoreToolCalls = toolCalls.length > 0;

      // 4. 执行工具 + steering 检查
      if (hasMoreToolCalls) {
        const toolExecution = await executeToolCalls(/* ... */);
        steeringAfterTools = toolExecution.steeringMessages ?? null;
        // 结果写回上下文
        for (const result of toolExecution.toolResults) {
          currentContext.messages.push(result);
        }
      }

      // 5. 获取新的 steering 消息
      pendingMessages = steeringAfterTools || (await config.getSteeringMessages?.()) || [];
    }

    // Agent 准备停止 → 检查 follow-up
    const followUpMessages = (await config.getFollowUpMessages?.()) || [];
    if (followUpMessages.length > 0) {
      pendingMessages = followUpMessages;
      continue;  // 继续外层循环
    }
    break;  // 退出
  }
}

这就是 ReAct(Reasoning + Acting)模式——LLM 生成推理和工具调用,执行工具,将结果回填上下文,再让 LLM 基于新信息继续推理。Yao et al., 2022 提出的,当前绝大多数 Agent 框架的基础架构。

值得注意的是 streamAssistantResponse 的实现。它是 AgentMessage[] 和 LLM 之间的转换层:

async function streamAssistantResponse(context, config, signal, stream, streamFn) {
  // 1. 先做 AgentMessage[] → AgentMessage[] 的上下文变换(如果配置了 transformContext)
  let messages = context.messages;
  if (config.transformContext) {
    messages = await config.transformContext(messages, signal);
  }

  // 2. 再做 AgentMessage[] → Message[] 的 LLM 格式转换
  const llmMessages = await config.convertToLlm(messages);

  // 3. 构建 LLM 上下文并发起请求
  const llmContext = {
    systemPrompt: context.systemPrompt,
    messages: llmMessages,
    tools: context.tools,
  };
  const response = await streamFunction(config.model, llmContext, { ...config, apiKey, signal });
  // ...streaming 处理
}

pi-mono 在内部维护了一套自定义的 AgentMessage 类型(包含 bashExecutionbranchSummarycompactionSummary 等角色),只在调用 LLM 的边界处才通过 convertToLlm 转换为标准的 Message[]。这个设计很干净——内部用丰富的类型系统追踪上下文,对外只暴露 LLM 能理解的标准格式。

pi-mono 的 ReAct 实现确实没有多余的抽象层。但从架构模式角度,它和其他 ReAct 实现没有本质区别。

工具集:不是 4 个,是 7 个

一些介绍文章声称 pi-mono 只有 read / edit / write / bash 四个”原语”。实际代码(packages/coding-agent/src/core/tools/index.ts):

// Default tools for full access mode (using process.cwd())
export const codingTools: Tool[] = [readTool, bashTool, editTool, writeTool];

// Read-only tools for exploration without modification (using process.cwd())
export const readOnlyTools: Tool[] = [readTool, grepTool, findTool, lsTool];

// All available tools (using process.cwd())
export const allTools = {
    read: readTool, bash: bashTool, edit: editTool, write: writeTool,
    grep: grepTool, find: findTool, ls: lsTool,
};

实际有 7 个工具codingTools 默认导出 4 个,readOnlyTools 导出另外 4 个(read、grep、find、ls),allTools 导出全部 7 个。

并且系统提示的生成逻辑(system-prompt.ts)会根据当前加载的工具集动态生成 guidelines。当 grepfindlsbash 同时存在时:

if (hasBash && (hasGrep || hasFind || hasLs)) {
    guidelinesList.push(
      "Prefer grep/find/ls tools over bash for file exploration (faster, respects .gitignore)"
    );
}

系统提示明确要求优先使用 grep/find/ls 而不是 bash 来做文件探索——这些工具在实际使用中不可或缺。

“极简四原语”是选择性呈现。这不是什么大问题,但它说明了一个值得警惕的现象:当我们用”哲学”来描述工程实现时,很容易把事实往叙事需要的方向裁剪。

bash 工具:无任何执行策略

深入看 bash.ts 的实现,bash 工具本质上就是一个直接的 spawn

const child = spawn(shell, [...args, command], {
    cwd,
    detached: true,
    env: env ?? getShellEnv(),
    stdio: ["ignore", "pipe", "pipe"],
});

LLM 生成什么命令就执行什么,没有命令白名单/黑名单,没有执行前审查,没有沙箱。唯一的安全机制是用户可选地传入 timeout 参数(单位秒),超时后 killProcessTree 杀掉进程。输出超过 DEFAULT_MAX_BYTES(默认 30KB)时会做尾部截断,完整输出写入临时文件。

这个设计在用户在场的交互式场景下是合理的——你在终端前看着它执行,发现不对可以随时 Ctrl+C。但在无人值守场景下,这意味着 Agent 有完整的系统访问权限且没有任何自动化的安全约束。后面会看到 Codex 在同样的 bash 执行上做了什么。

Steering:约 10 行代码的中断机制

Steering 在介绍文章中被大篇幅渲染。实际实现在 executeToolCalls 函数中,我们来完整看一下工具执行的流程:

async function executeToolCalls(tools, assistantMessage, signal, stream, getSteeringMessages) {
  const toolCalls = assistantMessage.content.filter((c) => c.type === "toolCall");
  const results = [];
  let steeringMessages;

  for (let index = 0; index < toolCalls.length; index++) {
    const toolCall = toolCalls[index];
    const tool = tools?.find((t) => t.name === toolCall.name);

    // 验证参数 → 执行工具
    const validatedArgs = validateToolArguments(tool, toolCall);
    result = await tool.execute(toolCall.id, validatedArgs, signal, onUpdate);
    results.push(/* toolResultMessage */);

    // ← 关键:每执行完一个工具,检查 steering
    if (getSteeringMessages) {
      const steering = await getSteeringMessages();
      if (steering.length > 0) {
        steeringMessages = steering;
        // 跳过剩余全部工具调用
        const remainingCalls = toolCalls.slice(index + 1);
        for (const skipped of remainingCalls) {
          results.push(skipToolCall(skipped, stream));
        }
        break;
      }
    }
  }
  return { toolResults: results, steeringMessages };
}

被跳过的工具调用会收到一个 isError: true 的结果,内容是 "Skipped due to queued user message."。这些结果连同已执行工具的结果一起回填到上下文中,然后回到 runLoop 的内层循环——此时 pendingMessages 中有了用户的 steering 消息,会在下一轮 LLM 调用前注入上下文,让 LLM 基于新的用户指示重新决策。

实用的工程细节,本质上就是一个回调检查点。Claude Code 等工具也有类似的中断机制。把它包装成”截拳道的截”纯粹是修辞。

Compaction:Goal-first 摘要,深入看实现

上下文接近窗口上限时触发压缩。核心逻辑在 packages/coding-agent/src/core/compaction/compaction.ts,值得展开看几个关键细节。

Token 估算用的是 chars / 4 的启发式,逐 block 累加:

export function estimateTokens(message: AgentMessage): number {
  let chars = 0;
  switch (message.role) {
    case "assistant": {
      for (const block of assistant.content) {
        if (block.type === "text") chars += block.text.length;
        else if (block.type === "thinking") chars += block.thinking.length;
        else if (block.type === "toolCall")
          chars += block.name.length + JSON.stringify(block.arguments).length;
      }
      return Math.ceil(chars / 4);
    }
    // ... 其他角色类似处理
    // 图片估算为 4800 chars(≈1200 tokens)
  }
}

chars / 4 是一个非常粗略的估算。不同语言(中文 vs 英文)、不同 tokenizer(BPE vs SentencePiece)的 chars/token 比率差异很大。代码注释自己也承认 “This is conservative (overestimates tokens)“。但 pi-mono 也做了一个务实的优化:在有 LLM 返回 usage 数据的情况下,estimateContextTokens 会优先使用 LLM 报告的真实 token 数,只对 usage 之后的新消息用启发式估算。

触发条件的默认参数值得记一下:

export const DEFAULT_COMPACTION_SETTINGS: CompactionSettings = {
    enabled: true,
    reserveTokens: 16384,    // 为新对话预留的 token 空间
    keepRecentTokens: 20000, // 压缩后保留的最近消息 token 量
};

shouldCompact 的判断是 contextTokens > contextWindow - reserveTokens。也就是当上下文用量超过窗口大小减去 16384 时触发。

切割点算法不是简单地从尾部截断。findCutPoint 从最新消息往前走,累加 token 估算,超过 keepRecentTokens(默认 20000)后找到最近的合法切割点。合法切割点是 userassistantcustombashExecution 类型的消息——永远不会在 toolResult 处切割,因为 toolResult 必须紧跟其对应的 toolCall。

更有意思的是分裂轮次处理。如果切割点落在一个 turn 的中间(比如一个 user 消息引发了 LLM 的多步工具调用,切割点在第 3 步),pi-mono 会生成两份摘要:一份是切割点之前的完整历史摘要,另一份是被分裂 turn 的前缀摘要(TURN_PREFIX_SUMMARIZATION_PROMPT),两者并行生成后合并:

if (isSplitTurn && turnPrefixMessages.length > 0) {
    const [historyResult, turnPrefixResult] = await Promise.all([
        generateSummary(messagesToSummarize, model, ...),
        generateTurnPrefixSummary(turnPrefixMessages, model, ...),
    ]);
    summary = `${historyResult}\n\n---\n\n**Turn Context (split turn):**\n\n${turnPrefixResult}`;
}

摘要模板用了一套结构化格式:

## Goal
## Constraints & Preferences
## Progress (Done / In Progress / Blocked)
## Key Decisions
## Next Steps
## Critical Context

后续压缩使用 UPDATE_SUMMARIZATION_PROMPT,不重写摘要,而是在旧摘要基础上合并更新。规则中要求 “PRESERVE all existing information”,同时 “UPDATE the Progress section: move items from ‘In Progress’ to ‘Done’ when completed”。生成时设了 reasoning: "high" 来提高推理强度,maxTokens 设为 reserveTokens * 0.8(即约 13107 tokens)。

此外,compaction 还会追踪文件操作。extractFileOpsFromMessage 遍历所有 assistant 消息中的 toolCall,根据工具名(read/write/edit)提取涉及的文件路径,追加到摘要末尾。这让 compaction 后的摘要保留了”哪些文件被读过/改过”的信息。

但没有做的事情同样重要:没有摘要质量验证(no second-pass check),没有回退机制(摘要生成后直接替换旧历史),没有用户确认环节。reasoning: "high" 不等于质量保证——它只是一个参数,无法消除信息丢失的可能性。

会话树:基于 JSONL 的 append-only 分支

pi-mono 的会话用 JSONL 文件存储。每条记录都是一个 SessionEntry,带 id(8 位 hex 短 UUID)和 parentId,形成树结构:

export interface SessionEntryBase {
    type: string;
    id: string;             // 8 位 hex, 由 randomUUID().slice(0, 8) 生成
    parentId: string | null; // null 表示根节点
    timestamp: string;
}

SessionManager 维护一个 leafId 指针,指向当前会话路径的末端节点。所有 appendXXX() 操作都是 append-only——新条目的 parentId 设为当前 leafId,写入后更新 leafId 为新条目的 id

分支操作 branch() 极其简单——5 行代码,就是把 leafId 指针回退到历史某个节点:

branch(branchFromId: string): void {
    if (!this.byId.has(branchFromId)) {
        throw new Error(`Entry ${branchFromId} not found`);
    }
    this.leafId = branchFromId;
}

后续新消息的 parentId 会指向这个节点,自然形成新分支。旧分支的数据仍然在文件中,只是不在当前路径上了。

branchWithSummary() 在回退时附加一条 branch_summary 类型的条目,让 LLM 对被放弃路径有个交代。forkFrom() 更进一步——从一个已有的会话文件创建一个全新的会话文件(带新 header 和新 session ID),通过 parentSession 字段指向源会话。

读取会话上下文时,buildSessionContextleafId 沿 parentId 链回溯到根节点,只取路径上的条目:

// Walk from leaf to root, collecting path
const path: SessionEntry[] = [];
let current: SessionEntry | undefined = leaf;
while (current) {
    path.unshift(current);
    current = current.parentId ? byId.get(current.parentId) : undefined;
}

工程上简洁的实现——append-only 文件操作 + 指针回退来模拟分支,避免了数据库依赖。代价是文件会随着分支增多而持续增长(废弃分支的数据永远不会被删除),但对于 coding agent 的使用周期来说这通常不是问题。


设计取舍

把上面拆解的模块放在一起看,它们共享一个前提:用户在场

bash 直接 spawn——用户在终端前看着,不对就 Ctrl+C。Steering 是回调检查点——用户主动发消息纠偏。终止条件是 LLM 停下就停下——用户自己判断结果对不对。Compaction 没有质量验证——用户发现偏差可以重新提醒。会话树的 branch() 支持回退——用户手动操作。

当这个前提成立时,这些设计都是合理的,某些甚至是最优的——没有审批弹窗打断心流,没有策略规则减慢执行,没有强制输出格式限制灵活性。Steering 解决了”LLM 跑过头”的真实痛点,/fork 让试错成本很低。在开发者终端前边想边做的场景下,pi-mono 是一个设计上自洽的 coding agent。

但”用户在场”是一个很强的假设。当它不成立时,pi-mono 的每一项设计优势都变成了空白——steering 回调永远返回空数组,bash 有完整系统权限无人监管,LLM 随时可能在未完成时停下,compaction 的信息丢失无人发现。这不是缺陷——pi-mono 本来就不是为这些场景设计的。但看看其他项目在同样的设计决策上怎么做的,能更清楚地理解不同场景需要什么样的工程。

行动空间:放开还是管住

pi-mono 的行动空间在启动时确定——codingTools(4 个)或 readOnlyTools(4 个),运行过程中不可调整。bash 工具不做任何执行前审查。

Codex 在同样的 bash 执行上做了完全不同的选择——对每条 shell 命令做执行前的三级评估:

pub(crate) async fn create_exec_approval_requirement_for_command(&self, req: ExecApprovalRequest<'_>)
    -> ExecApprovalRequirement
{
    let exec_policy = self.current();
    let commands = parse_shell_lc_plain_commands(command)
        .unwrap_or_else(|| vec![command.to_vec()]);
    let evaluation = exec_policy.check_multiple(commands.iter(), &exec_policy_fallback);

    match evaluation.decision {
        Decision::Forbidden => ExecApprovalRequirement::Forbidden {
            reason: derive_forbidden_reason(command, &evaluation),
        },
        Decision::Prompt => {
            if matches!(approval_policy, AskForApproval::Never) {
                ExecApprovalRequirement::Forbidden { reason: PROMPT_CONFLICT_REASON.to_string() }
            } else {
                ExecApprovalRequirement::NeedsApproval {
                    reason: derive_prompt_reason(command, &evaluation),
                    proposed_execpolicy_amendment: /* 提议添加的规则 */,
                }
            }
        },
        Decision::Allow => ExecApprovalRequirement::Skip {
            bypass_sandbox: /* 策略明确允许时可以绕过沙箱 */,
        },
    }
}

策略文件(.rules)按层级叠加(全局 → 项目 → 本地),管道或 && 连接的命令被拆分后逐一检查。在此之上还有 OS 级沙箱(macOS seatbelt / Linux landlock),从操作系统层面兜底。

AskForApproval::Never 分支值得注意:配置为”永不询问”时,需要审批的命令直接变 Forbidden——不是默认通过,而是保守拒绝。这是无人值守场景下唯一安全的默认行为。

Kimi-CLI 在另一个粒度做约束——不是约束单条命令,而是约束整个 Agent 角色的能力边界。每个 Agent 的工具集由 YAML 声明:

# 主 Agent 配置(agent.yaml)
agent:
  tools:
    - "kimi_cli.tools.multiagent:Task"       # 可以创建子 Agent
    - "kimi_cli.tools.todo:SetTodoList"       # 可以管理待办
    - "kimi_cli.tools.shell:Shell"
    - "kimi_cli.tools.file:ReadFile"
    - "kimi_cli.tools.file:WriteFile"
    - "kimi_cli.tools.file:StrReplaceFile"
    - "kimi_cli.tools.web:SearchWeb"
    # ...
  subagents:
    coder:
      path: ./sub.yaml
      description: "Good at general software engineering tasks."
# 子 Agent 配置(sub.yaml)
agent:
  extend: ./agent.yaml          # 继承主 Agent 的基础配置
  exclude_tools:                 # 但裁剪掉这些能力
    - "kimi_cli.tools.multiagent:Task"        # 禁止创建子 Agent
    - "kimi_cli.tools.multiagent:CreateSubagent"
    - "kimi_cli.tools.dmail:SendDMail"
    - "kimi_cli.tools.todo:SetTodoList"
  subagents:                     # 子 Agent 没有自己的子 Agent

子 Agent 通过 extend 继承主 Agent 工具集,exclude_tools 显式禁止 TaskCreateSubagent——在配置层面切断无限递归。subagents 留空,确保不能再委派。这不是”让 LLM 自己判断要不要递归”,而是声明式的硬约束。

OpenAI Agents SDK 做的是工具级的动态约束——每个 Agent 实例绑定独立的 toolsis_enabled 支持运行时按上下文启用/禁用,Handoff 让不同 Agent 各持不同工具集:

@dataclass
class Agent(AgentBase, Generic[TContext]):
    tools: list[Tool] = field(default_factory=list)          # 每个 Agent 独立的工具集
    handoffs: list[Agent | Handoff] = field(default_factory=list)  # 可以委派的子 Agent
    output_type: type[Any] | None = None                     # 结构化输出约束

三种粒度的对比:

约束粒度实现特点
工具级OpenAI Agents SDK(per-agent tools, is_enabled)灵活,适合多 Agent 编排
角色级Kimi-CLI(AgentSpec YAML, exclude_tools)声明式,子 Agent 自动继承+裁剪
命令级Codex(ExecPolicy rules, sandbox)最细粒度,安全性最高

pi-mono 不做运行时约束,不是疏忽——在用户在场时,约束机制是多余的摩擦。但这也意味着在无人值守时,没有任何自动化手段能限制 Agent “只做与当前任务相关的操作”。

纠偏:谁来拦

pi-mono 的 steering 依赖用户发现问题后主动介入。系统主动拦截是另一种思路——Kimi-CLI 的 Approval 类实现了三级响应:

async def request(self, sender, action, description, display=None) -> bool:
    # 快速路径 1:yolo 模式 → 一律通过
    if self._state.yolo:
        return True

    # 快速路径 2:该 action 已被标记为 session 自动通过
    if action in self._state.auto_approve_actions:
        return True

    # 慢路径:创建审批请求,等待用户响应
    request = Request(id=str(uuid.uuid4()), tool_call_id=tool_call.id, ...)
    approved_future = asyncio.Future[bool]()
    self._request_queue.put_nowait(request)
    self._requests[request.id] = (request, approved_future)
    return await approved_future

用户响应时:

def resolve_request(self, request_id, response):
    match response:
        case "approve":                # 本次通过
            future.set_result(True)
        case "approve_for_session":    # 本会话后续同类操作自动通过
            self._state.auto_approve_actions.add(request.action)
            future.set_result(True)
        case "reject":                 # 拒绝
            future.set_result(False)

approve_for_session 是个巧妙的平衡——第一次确认后同类操作会话内自动通过,兼顾安全和效率。yolo 模式跳过全部审批,但这是用户的显式选择,不是默认行为。Approval 对象支持 share() 创建共享状态的新实例,子 Agent 可以继承主 Agent 的审批状态。

Codex 的 Decision::Prompt 同样是系统主动审批——用户批准后可将命令前缀写入 .rules 文件永久生效。

两种机制解决的层面不同。Steering 是方向纠偏——“你在做错的事,换个方向”。审批是操作准入——“这个操作能不能做”。前者需要用户持续在场并具备判断力,后者可以通过预定义规则自动化。

何时停下,结果对不对

pi-mono 的终止条件:LLM 不再生成 toolCalls。没有步数限制,没有输出验证,没有超时——完全信任 LLM 自行判断”做完了”。

OpenAI Agents SDK 提供了确定性的终止和验证。output_type 要求最终输出符合预定义的 Pydantic model,StopAtTools 在调用特定工具时停止,OutputGuardrail 在验证不通过时中断执行:

@dataclass
class OutputGuardrail(Generic[TContext]):
    guardrail_function: Callable[..., GuardrailFunctionOutput]

这些是硬编码的程序逻辑,不依赖 LLM 的自我判断。

Kimi-CLI 的做法更朴素但同样是确定性检查。看 task.py 中子 Agent 的完整执行流程:

async def _run_subagent(self, agent, prompt):
    # 1. 执行子 Agent
    try:
        await run_soul(soul, prompt, _ui_loop_fn, asyncio.Event())
    except MaxStepsReached as e:
        # 硬性步数上限 → 直接返回错误
        return ToolError(
            message=f"Max steps {e.n_steps} reached when running subagent. "
                    "Please try splitting the task into smaller subtasks.",
        )

    # 2. 验证子 Agent 的输出
    if len(context.history) == 0 or context.history[-1].role != "assistant":
        return ToolError(message="The subagent seemed not to run properly. "
                                "Maybe you have to do the task yourself.")

    # 3. 检查输出长度 → 太短则要求补充
    final_response = context.history[-1].extract_text(sep="\n")
    if len(final_response) < 200 and n_attempts_remaining > 0:
        await run_soul(soul, CONTINUE_PROMPT, _ui_loop_fn, asyncio.Event())
        # CONTINUE_PROMPT 要求: "Please provide a more comprehensive summary
        #  that includes specific technical details and implementations..."
        final_response = context.history[-1].extract_text(sep="\n")

    return ToolOk(output=final_response)

MaxStepsReached 防无限循环,输出角色检查防异常终止,200 字符阈值 + continuation prompt 防子 Agent 草草了事——每一层都是确定性逻辑,不依赖 LLM 的”自我反思”。

失败处理也是类似的分野。pi-mono 的工具错误被包装成 isError: true 的 toolResult 回填上下文,LLM 自行决定怎么恢复——没有结构化的重试逻辑,没有回滚机制。会话树的 branch() 支持回退,但完全依赖用户手动操作。Kimi-CLI 的子 Agent 失败后返回结构化的 ToolError(包含具体失败原因),主 Agent 根据错误类型决定重试、拆分或自己处理。OpenAI Agents SDK 的 Handoff 则是另一条路——一个 Agent 处理不了,委派给能力不同的 Agent。不是完美的自动化回退,但至少有结构化的失败传播路径。


写在最后

pi-mono 是一个工程上简洁的 ReAct agent 实现——agent loop 干净、compaction 有分裂轮次处理和文件操作追踪这些实际巧思、会话树用 append-only + 指针回退避免了数据库依赖。它的每个设计选择都指向同一个前提:用户在场。在这个前提下,它是一个合理且好用的工具。

但它不是什么”截拳道哲学”,也不是什么”极简原语的组合艺术”。它就是标准 ReAct 循环、7 个工具(默认暴露 4 个)、一个回调检查点、一个 Goal-first 模板的上下文压缩、一个基于 JSONL 的会话分支结构。每一项都有据可查的前置工作和广泛的行业实践。

你的目标场景在”容忍探索”和”要求确定性收敛”的光谱上处于什么位置?你的架构设计是否匹配了这个位置的需求?

两种场景没有高下之分,但混淆二者的需求、用一种场景的成功去论证另一种场景的可行性,则是彻头彻尾的不负责且未经审视思考的输出。

Previous
Bub Tape 架构深度解读:从可追踪记忆到可控上下文窗口
Next
Next-generation large language model interface architecture