一个更有用的分类框架
讨论 Agent 架构时,常见的切入角度是”个人 vs 商业”、“轻量 vs 重量”、“极简 vs 复杂”。这些直觉上有道理,但在分析具体设计决策时缺乏解释力——同一个架构可以既适合某些个人场景,又适合某些商业场景,反之亦然。
更有区分度的维度是:场景对结果的要求。
- 容忍探索的场景:用户在场、愿意参与过程、可以随时纠偏。不确定性是可接受的,甚至是有价值的。比如开发者用 coding agent 做技术探索,日常开发中边调试边迭代。
- 要求确定性收敛的场景:需要结果快速、稳定地收敛到可预测的输出。不确定性是成本。比如 CI/CD 中的自动化修复、面向客户的产品功能、批量代码迁移。
这两种场景对 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.ts 的 runLoop 函数中。先看结构骨架:
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类型(包含bashExecution、branchSummary、compactionSummary等角色),只在调用 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。当 grep、find、ls 与 bash 同时存在时:
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)后找到最近的合法切割点。合法切割点是 user、assistant、custom、bashExecution 类型的消息——永远不会在 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 字段指向源会话。
读取会话上下文时,buildSessionContext 从 leafId 沿 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 显式禁止 Task 和 CreateSubagent——在配置层面切断无限递归。subagents 留空,确保不能再委派。这不是”让 LLM 自己判断要不要递归”,而是声明式的硬约束。
OpenAI Agents SDK 做的是工具级的动态约束——每个 Agent 实例绑定独立的 tools,is_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 的会话分支结构。每一项都有据可查的前置工作和广泛的行业实践。
你的目标场景在”容忍探索”和”要求确定性收敛”的光谱上处于什么位置?你的架构设计是否匹配了这个位置的需求?
两种场景没有高下之分,但混淆二者的需求、用一种场景的成功去论证另一种场景的可行性,则是彻头彻尾的不负责且未经审视思考的输出。