本文档基于实际业务的抽象基础框架,我们给他起了个名字:reins。体现"收敛控制权 + 预算约束",谐音"reigns(统治)“双关。 为了不涉及具体业务,相关业务用”A业务“代替
0. 定位澄清:规范 vs 框架,以及为什么不直接用 Eino
0.1 为什么不直接用 Eino
Eino(字节跳动,Go 生态)是目前 Go 技术栈最成熟的 Agent 编排框架,与本方案最直接竞争。以下是认真评估后的结论:
Eino 擅长的部分(可以直接复用):
- 强类型 Graph/Chain 编排,利用 Go 泛型保证组件类型安全
- ChatModel、Tool、Retriever 等基础组件抽象
- 流式输出(Streaming)管道
Eino 没有解决、也不打算解决的部分(本方案的真正价值所在):
| 问题 | Eino 的立场 | 本方案的解法 |
|---|---|---|
| 预算治理:token/time/retry 如何跨 Agent 统一封顶? | 不内置,由调用方自行实现 | WorkflowState + BudgetManager,编排层统一持有预算,禁止 Agent 私自重试 |
| 审批式 Handoff:Agent 提出的协作建议如何经过审批再执行? | 不支持,Eino 的 Handoff 是直接转交 | Handoff 仅为建议,Orchestrator 审批后才执行,防环、防成本放大 |
| 会话级配置快照:热更新时如何保证同一 workflow 内行为不漂移? | 不内置,框架无配置版本化概念 | config_snapshot_id 绑定到 workflow,全程读取同一快照 |
| 可观测性作为协议:UsageStats 是否强制随每次输出上报? | 可选,各组件实现程度不一 | AgentOutput.Usage 是必填字段,不填则协议不完整 |
结论:Eino 解决的是"如何把 LLM 调用和 Tool 调用组合起来”,本方案解决的是"如何在生产中治理 Agent 的预算、控制权、配置一致性和可观测性"。两者不重叠,可以共存——**推荐在 Agent 内部实现中使用 Eino 的 ChatModel/Tool 抽象,而不是自建模型调用层;但 Orchestrator、WorkflowState、BudgetManager、ConfigSnapshot 这些治理机制仍需自建。
0.2 五个横切关注点的差异化评估
挑刺者指出"只有预算治理和可观测规范有差异化价值",这个判断基本成立:
| 横切关注点 | 成熟框架是否解决 | 差异化程度 | 结论 |
|---|---|---|---|
| 控制权收敛(审批式 Handoff) | 部分——LangGraph 有审批节点,Eino 不支持 | 高:Go 生态无现成方案 | 必须自建 |
| 单一状态源(WorkflowState 持久化) | 部分——框架提供内存状态,不负责持久化 | 中:持久化方案各家不同 | 必须自建 |
| 预算驱动执行 | 否——所有主流框架均不内置跨 Agent 预算治理 | 高 | 必须自建 |
| 最小上下文裁剪 | 部分——LangChain 有 memory 组件,但未解决摘要策略 | 中 | 自建裁剪策略 |
| 可观测强制规范(UsageStats 作为协议必填) | 否——框架提供 callback,但不强制 | 高 | 协议层强制 |
修订后的重点:方案不回避其他三个点(它们仍然需要设计),但明确标注预算治理、审批式 Handoff、可观测规范是本方案相对成熟框架的核心差异化价值。
1. 核心设计哲学
1.1 控制权收敛原则
Agent 只能表达意图,编排层拥有决策权。
Agent 可以提出 handoff 建议,但不能自行移交控制权。Orchestrator 是唯一的运行时决策者。
为什么:Agent 间直接 handoff 在原型好用,但在生产中 WorkflowState 由谁持有、重试由谁发起、防环由谁检测都会产生歧义。控制权收敛是可观测性和可恢复性的根基。
1.2 单一状态源原则
WorkflowState 由编排层唯一持有,Agent 是无状态执行器。
所有运行时状态在编排层落库后再触发下一跳。Agent 接受输入、返回输出,不感知也不持有全局状态。
1.3 最小上下文原则
Agent 只获取完成当前任务所需的最小上下文,由编排层按需裁剪。
Agent 不自拉历史,编排层构造 ContextEnvelope 时主动裁剪。上下文越大,token 成本越高,注意力干扰越强;Agent 自拉上下文则使权限边界和成本都不可控。
1.4 预算驱动执行原则(核心差异化点)
所有资源消耗必须在启动前显式建模,不允许无上限执行。
每个 workflow 启动时分配总预算(deadline/token/retry),编排层同时下发分片预算。预算数值不注入原始值到 Prompt,而是由 Budget Interpreter 在触阈时转译为语义指令。
为什么这是差异化:LLM 调用 × 重试 × fallback × 修复回路的成本叠加效应极强。Eino/LangChain 等框架均不内置跨 Agent 的统一预算管控,这是本方案独立存在的核心理由之一。
1.5 可观测优先原则(核心差异化点)
可观测性是协议的一部分,而不是事后插件。
AgentOutput.Usage 是必填字段,缺失则协议不完整。TraceContext 的 6 个字段必须在所有内部调用中强制透传。
2. 整体架构
3. 核心抽象层设计
字段标注说明:
[通用]表示与业务无关、可直接复用;[A业务]表示包含业务假设,其他项目需根据自身场景调整或删除。
3.1 BaseAgent 接口
// AgentDescriptor 描述 Agent 能力,供意图路由和编排层使用
type AgentDescriptor struct {
Name string // [通用] 唯一标识
Description string // [通用] 能力描述(用于 LLM 路由)
InputTypes []string // [通用] 接受的 artifact 类型
OutputTypes []string // [通用] 产出的 artifact 类型
// [通用] 可选元数据,业务层通过 tags 携带自定义属性(如 risk_level、domain 等)
// 不在接口层内置业务字段,避免通用协议被业务假设污染
Tags map[string]string
}
// BaseAgent 是所有 Agent 的统一接口
type BaseAgent interface {
Run(ctx context.Context, input ContextEnvelope) (AgentOutput, error)
Describe() AgentDescriptor
}
关于 RiskLevel 的修订:初稿中 AgentDescriptor.RiskLevel 是 A业务 特有的安全分级概念,不应内置到通用接口层。改为通过 Tags["risk_level"] 携带,业务层自行解读;不需要此字段的项目不受影响。
设计原则:
- 单一入口:
Run是唯一执行入口,防止编排层绕过协议调用内部方法 - 无状态约定:
Run不依赖实例内部状态,同等输入在任意实例上产生等价输出 - 可扩展元数据:
Tags替代内置业务字段,接口稳定,业务语义在调用方自行解释
3.2 意图路由协议(RouteDecision)
// [通用]
type RouteDecision struct {
TargetAgent string // 主路由目标 Agent 名称
SecondaryHints []string // 接力意图声明(Orchestrator 消费,不透传 Agent)
RouteReason string // 路由原因,用于审计
Confidence string // "high" | "medium" | "low"
RuleID string // 命中的规则 ID,空字符串表示 LLM 兜底
ModelVersion string // LLM 路由时使用的模型版本
IsFallback bool // 是否为兜底路由
}
关键设计决策:
SecondaryHints仅供 Orchestrator 消费,不透传给 AgentConfidence是 Orchestrator 的风险信号:high直接执行,medium激活结果验证钩子,low保守路由RuleID + ModelVersion + Confidence三元组是路由层可追溯性基础
路由策略(两级):
3.3 工作流状态模型(WorkflowState)
WorkflowState 是系统唯一运行时状态源,由编排层持有和持久化。
type WorkflowStatus string
// [通用] 核心状态
const (
StatusPending WorkflowStatus = "pending"
StatusRouted WorkflowStatus = "routed"
StatusRunning WorkflowStatus = "running"
StatusWaitingHandoff WorkflowStatus = "waiting_handoff_approval"
StatusWaitingUserInput WorkflowStatus = "waiting_user_input"
StatusPartialFailed WorkflowStatus = "partial_failed"
StatusCompleted WorkflowStatus = "completed"
StatusFailed WorkflowStatus = "failed"
StatusCanceled WorkflowStatus = "canceled"
)
// [通用] WorkflowState 核心字段
type WorkflowState struct {
WorkflowID string // 全局唯一
SessionID string // "tenant:user:session"
TenantID string // 租户隔离键
Status WorkflowStatus
CurrentAgent string // 当前执行中的 Agent
VisitedAgents []string // 已访问 Agent 列表(防环检测)
StepIndex int // 当前步骤序号
DeadlineMs int64 // 工作流总截止时间戳(Unix ms)
RetryBudget int // 剩余重试次数
TokenBudget int // 剩余 token 预算
ConfigSnapshotID string // 绑定的配置快照(会话内不变)
ArtifactRefs []string // 已产出产物引用
TraceID string // 贯穿全链路的 Trace ID
// [扩展点] 业务层可通过此字段携带自定义状态,框架不感知
Extensions map[string]interface{}
}
关于 needs_manual_review 状态的修订:初稿将其内置为通用状态,但这是 A业务 表达式必须人工审核的业务约束,不应内置在通用状态机中。修订方案:通用状态机只内置上述 9 个状态;“等待人工审核"通过两种机制支持——①业务层在 Extensions 中携带标记;②Orchestrator 提供 ReviewHook 扩展点(见第 5.1 节),业务层注册后在合适时机触发。
状态机转换图:
3.4 Agent 输入协议(ContextEnvelope)
// [通用]
type ContextEnvelope struct {
TaskInstruction string // 针对当前 Agent 的具体任务指令
ConversationExcerpt []ConvTurn // 最近 N 轮原文(N 由 ContextBuilder 策略决定)
ConversationSummary string // 更早轮次的摘要(摘要策略见 5.2 节)
Artifacts []ArtifactRef // 上游结构化产物引用
SessionMeta SessionMetadata
Budgets ExecutionBudget
ExecutionHints *ExecutionHints // 可选,Orchestrator 按需填充
}
// [通用]
type ExecutionBudget struct {
DeadlineMs int64
TokenBudget int
}
// [通用] 执行提示,帮助 Agent 感知自身在 pipeline 中的位置
// 注意:这里使用开放字符串而非枚举,避免框架预设 pipeline 形态
type ExecutionHints struct {
// [通用] Agent 在当前调用链中的角色描述,影响输出详细程度
// 示例值:"first_in_chain" | "last_in_chain" | "standalone" | 业务自定义值
// 非线性 pipeline 的项目可使用不同的语义,框架不做强制约束
PipelineRole string
// [通用] 下游期望的产物类型,引导 Agent 在非终止节点时输出结构化数据
DownstreamExpects string
// [通用] 仅用于调试和可观测性,Agent 不应基于此做决策
ChainIntent string
}
关于 PipelineRole 的修订:初稿将 first_in_chain | last_in_chain 作为枚举写死,隐含了"线性 pipeline"假设。修订为开放字符串,框架不约束取值——线性 pipeline 项目用 first/last_in_chain,DAG 型项目可自定义语义(如 parallel_branch)。
预算注入策略:
| 条件 | 注入行为 |
|---|---|
| token_budget < 阈值(如 800) | 追加语义指令:response_constraint: 资源受限,给出简洁直接的回答 |
| deadline_ms < 阈值(如 3000ms) | 追加语义指令:response_constraint: 时间受限,跳过深度检索 |
| 均宽松 | 不注入任何内容 |
| Runtime 层 | 始终设置 max_tokens + HTTP timeout 作为硬约束,与 Prompt 无关 |
3.5 Agent 输出协议(AgentOutput)
// [通用]
type AgentOutput struct {
ProtocolVersion string // 协议版本
Status string // "success" | "partial" | "error"
Answer string // 面向用户的文字答案
Artifacts []Artifact
Handoff *HandoffSuggestion // 可选,仅为建议
Confidence ConfidenceSignal
Usage UsageStats // [通用] 必填,缺失视为协议不完整
Error *AgentError
NextAction *NextAction // 面向用户的下一步提示
}
// [通用]
type Artifact struct {
ID string
Type string // 业务层定义类型标签
Content json.RawMessage // 见下方说明
Metadata ArtifactMeta
}
// [通用] 必填
type UsageStats struct {
InputTokens int
OutputTokens int
ToolCalls int
LatencyMs int64
}
关于 Artifact.Content interface{} 的修订:
初稿用 interface{},ProtocolVersion 在序列化后形同虚设——反序列化时仍需 type switch,版本字段无法保护结构变化。
修订方案:将 Content 改为 json.RawMessage,配合 Type 字段实现按类型分发反序列化:
// 消费方按 Type 分发,得到强类型结构
switch artifact.Type {
case "feature_list":
var fl FeatureList
json.Unmarshal(artifact.Content, &fl)
case "code_snippet":
var cs CodeSnippet
json.Unmarshal(artifact.Content, &cs)
}
版本演进机制的真实保护:ProtocolVersion 保护的是外层信封结构(AgentOutput 的字段集合),而不是每个 Artifact 的内容结构。Artifact 内容结构的版本由 ArtifactMeta.ContentVersion 字段管理:
type ArtifactMeta struct {
Version string // Artifact 内容结构版本(如 "v1", "v2")
Source string
CreatedAt time.Time
}
版本演进规则:
- 外层信封(AgentOutput 字段集):新增字段向后兼容,删除/重命名字段升
ProtocolVersion - Artifact 内容结构:新增字段向后兼容,破坏性变更升
ArtifactMeta.Version,消费方按 version 分支处理 - 新 Artifact 类型:只新增
Type枚举值,老消费方收到未知 type 时静默忽略
这是软约定而非硬保护——框架层无法在编译期阻止消费方忽略版本字段。工程团队需通过 review 规范和集成测试来保证版本演进不破坏兼容性。
4. 关键机制设计
4.1 编排机制
模式:Supervisor 主控 + 审批式 Handoff
Handoff 通用约束:
- Agent 只能提出建议,Orchestrator 审批后才执行
- 禁止 A → B → C → A 循环(通过
visited_agents检测) - Orchestrator 拒绝 handoff 时,使用当前最优结果兜底
关于"最多一次 Handoff"的标注:初稿将此作为通用限制,但这是 A业务 第一阶段的业务约束(防止在 3 个 Agent 场景中链路过长),而非框架级规则。通用框架层提供 MaxHandoffDepth 配置项,默认值由业务层设置:
// [通用] Orchestrator 配置
type OrchestratorConfig struct {
// 最大 handoff 深度,0 表示不限制
// A业务 V1 推荐设为 1,其他场景可调整
MaxHandoffDepth int
// 其他配置...
}
ReviewHook 扩展点(替代内置 needs_manual_review 状态):
// [通用扩展点] 业务层注册,在 Agent 输出满足特定条件时触发人工审核流程
type ReviewHook interface {
// ShouldReview 判断当前输出是否需要人工确认
ShouldReview(output AgentOutput, state WorkflowState) bool
// OnReviewRequired 触发时的处理逻辑(如更新 Extensions、发送通知)
OnReviewRequired(ctx context.Context, output AgentOutput, state *WorkflowState) error
}
A业务 的表达式人工审核通过注册此 hook 实现,而非内置在通用状态机中。
4.2 上下文管理机制
裁剪策略(默认):
| 内容 | 策略 |
|---|---|
| 最近 2 轮对话 | 原文保留 |
| 2 轮以前的历史 | 摘要替代(策略见下) |
| 上游 Artifacts | 按 ID 引用或按 type 裁剪 |
| 错误/诊断日志 | 仅保留结构化摘要,不把完整 stderr 塞入 Prompt |
RouteDecision.* | 完全不进入 Prompt |
摘要策略(补充初稿的留白):
早期对话历史用摘要替代,但"摘要怎么做"是上下文管理中最容易出错的部分。提供两种默认实现供选择:
| 方案 | 实现 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| A: LLM 摘要 | 调用轻量模型对历史对话生成摘要 | 语义保真度高,不丢关键信息 | 引入额外 token 消耗和延迟 | 对话轮次多、上下文依赖强的场景 |
| B: 规则截断 | 保留最近 K 轮,超出则丢弃 | 零成本、零延迟、结果确定 | 早期关键信息可能丢失 | 轮次少、任务相对独立的场景 |
A业务 推荐选 B(规则截断):A业务 Agent 的每次 workflow 通常独立性较强(问答/特征搜索/表达式生成各自一个任务),跨 workflow 的长期记忆依赖度低,不值得为摘要引入额外的 LLM 调用成本。
摘要接口(支持扩展):
// [通用扩展点] 业务层可自定义摘要策略
type SummaryStrategy interface {
Summarize(ctx context.Context, turns []ConvTurn, budget TokenBudget) (string, error)
}
// [通用] 默认实现:规则截断(保留最近 K 轮,其余丢弃)
type TruncateSummary struct{ KeepLastK int }
// [可选] LLM 摘要实现,需业务层自行注入 LLMClient
type LLMSummary struct{ Client LLMClient; MaxSummaryTokens int }
ContextEnvelope → Prompt 渲染策略:
[System]
{agent_role_description}
当前租户:{tenant_id} ← 来自 SessionMeta,业务层按需注入
[User]
<context_envelope version="1.0">
<conversation_excerpt>...</conversation_excerpt>
<artifacts type="{artifact_type}">...</artifacts>
<execution_hints>
pipeline_role: {role}
downstream_expects: {type}
<!-- 触阈时追加 response_constraint -->
</execution_hints>
<task_instruction>
{具体任务指令,放在最后,优先级最高}
</task_instruction>
</context_envelope>
4.3 预算治理机制(核心差异化点)
预算分配规则:
| 预算类型 | 分配方式 | 说明 |
|---|---|---|
deadline_ms | 按步骤比例分配,保留 buffer | 总 deadline 减已消耗,按剩余步骤均分 |
token_budget | 按 Agent 角色分层设置 | 路由 « 问答生成 < 代码生成/修复 |
retry_budget | 按资源类型分开计数 | 禁止叠加放大,Agent 不能私自无限重试 |
预算耗尽处理:
deadline_ms耗尽:返回当前最优结果或partial状态token_budget耗尽:停止工具调用,用已有信息生成简洁回答retry_budget耗尽:返回最后一次结果或错误,不继续重试
4.4 工具注册与调用机制
// [通用]
type Tool interface {
Name() string
Description() string
InputSchema() Schema
Execute(ctx context.Context, input map[string]interface{}) (interface{}, error)
}
// [通用]
type ToolRegistry interface {
Register(tool Tool) error
Get(name string) (Tool, error)
ListFor(agentName string) []Tool // 按 Agent 权限返回可用工具列表
}
工具调用原则:
- 工具通过注册表统一管理,Agent 不直接实例化工具依赖
- 工具执行必须继承
ContextEnvelope.Budgets的超时约束 - 工具调用失败返回结构化错误,不允许 panic 或静默失败
- 工具调用次数计入
AgentOutput.Usage.ToolCalls
4.5 LLM 网关抽象
// [通用] 屏蔽厂商差异
type LLMClient interface {
Chat(ctx context.Context, req ChatRequest) (ChatResponse, error)
ChatStream(ctx context.Context, req ChatRequest) (<-chan ChatDelta, error)
Embed(ctx context.Context, texts []string) ([][]float32, error)
}
// [通用]
type ChatRequest struct {
Messages []Message
Tools []ToolSchema
MaxTokens int
Temperature float32
ModelHints ModelHints
}
// [通用] 用意图表达偏好,网关决定实际路由
type ModelHints struct {
Tier string // "economy" | "standard" | "premium"
UseCase string // "generation" | "embedding" | "rerank" | 业务自定义值
// [说明] "classification" 是 Router 特有场景,其他场景不一定需要
// UseCase 为开放字符串,不强制枚举,各业务按实际模型分层定义
}
为什么用 Tier 而非具体模型名:硬编码模型名会使切换模型需要修改 Agent 代码。用 Tier 表达意图,网关负责实际路由,Agent 对模型版本迭代免疫。
与 Eino 的关系:此接口可以直接用 Eino 的 ChatModel 实现,不需要重建模型调用层。
4.6 可观测性机制(核心差异化点)
全链路 Trace 字段(所有内部调用强制透传):
// [通用] 必须贯穿全链路
type TraceContext struct {
TraceID string // 全链路追踪
SessionID string
WorkflowID string
TenantID string
AgentName string // 当前执行节点
ModelName string // 实际调用的模型
ConfigSnapshotID string // 绑定的配置快照版本
}
核心指标维度:
| 维度 | 关键指标 |
|---|---|
| 路由层 | 路由准确率、LLM 兜底率 |
| 执行层 | P95/P99 延迟、Agent 错误率、handoff 触发率、fallback 触发率 |
| 成本层 | token 消耗(按 tenant/agent/model 分组)、工具调用次数 |
| 质量层 | 置信度分布、partial 完成率 |
流式事件协议:
// [通用]
type StreamEvent struct {
EventID string `json:"event_id"`
WorkflowID string `json:"workflow_id"`
Seq int `json:"seq"` // 严格递增,用于乱序检测
EventType string `json:"event_type"`
Timestamp time.Time `json:"timestamp"`
Payload json.RawMessage `json:"payload"` // 与 Artifact.Content 保持一致,用 RawMessage
Final bool `json:"final"`
}
// [通用] 内置事件类型
const (
EventWorkflowAccepted = "workflow.accepted"
EventRouteResolved = "route.resolved"
EventAgentStarted = "agent.started"
EventTextDelta = "text.delta"
EventArtifactReady = "artifact.ready"
EventUserInputRequired = "user_input.required"
EventWorkflowCompleted = "workflow.completed"
EventWorkflowFailed = "workflow.failed"
// [扩展] 业务层可自定义事件类型,前缀用业务域名隔离,如 "A业务.expr.validation.completed"
)
4.7 安全机制
四层安全模型:
| 安全层 | 机制 | 标注 |
|---|---|---|
| 输入侧 | Prompt Injection 检测、危险关键词拦截 | [通用] |
| 租户隔离 | tenant_id 贯穿路由/检索/上下文/审计 | [通用] |
| 执行侧 | 工具白名单、工具调用鉴权 | [通用] |
| 输出侧 | 敏感数据泄漏检测 | [通用] |
| 代码执行安全 | 沙箱隔离、函数白名单 | [A业务] 仅表达式场景需要 |
5. 扩展点设计
// [通用] 编排层扩展点接口集合
type OrchestratorExtensions struct {
// 上下文摘要策略
SummaryStrategy SummaryStrategy
// 人工审核钩子(替代内置 needs_manual_review 状态)
ReviewHook ReviewHook
// Handoff 审批策略(允许业务层自定义审批逻辑)
HandoffApprover HandoffApprover
// 结果聚合策略(多 Agent 输出如何合并为最终答案)
ResultAggregator ResultAggregator
}
// [通用] Handoff 审批策略接口
type HandoffApprover interface {
Approve(suggestion HandoffSuggestion, state WorkflowState) (bool, string)
}
扩展规则:
- 新 Agent:实现
BaseAgent,注册到编排层,无需修改编排逻辑 - 新 Tool:实现
Tool,注册到ToolRegistry - 新路由规则:通过 ConfigCenter 热更新,无需重启
- 新 Artifact 类型:新增
Type枚举值,老消费方收到未知 type 时静默忽略 - 模型切换:修改网关路由配置,Agent 代码零感知
- 人工审核:注册
ReviewHook,而不是修改状态机 - 摘要策略:注入
SummaryStrategy实现,支持 LLM 摘要或规则截断
6. 配置治理
配置分类与治理策略
| 配置类别 | 内容 | 真源 | 热更新 |
|---|---|---|---|
| 路由规则 | 关键词匹配、LLM 模型选择 | Git | 是(读新快照) |
| Prompt 模板 | System Prompt、Instruction 模板 | Git | 是(读新快照) |
| Agent 策略 | timeout/retry/token budget | Git | 是(读新快照) |
| 模型路由 | 模型分层映射 | Git | 是(读新快照) |
会话级配置快照:
- Workflow 启动时绑定当前
config_snapshot_id,全程读取同一快照 - 回滚时按 snapshot 版本回滚,而非直接改线上值
- 这一机制是框架独立存在的核心价值之一(Eino 不内置此能力)
7. 评测与发布门禁
三层质量门禁
评测闭环:
- Bad Case 回灌:线上失败样本、误路由样本进入评测集
- 评测结果驱动优化,不靠直觉改 Prompt
各层评测指标
| 评测对象 | 核心指标 |
|---|---|
| 意图路由 | 路由准确率、LLM 兜底率 |
| Agent 质量 | 任务完成率、置信度分布、partial 率 |
| 工具调用 | 工具成功率、平均调用次数 |
| 成本 | 每次 workflow 平均 token 消耗 |
后记
我认为:在多 Agent 的生产系统里,可控性是效率的前提,而不是效率的对立面。
不可控的自治,最终会把工程师的时间消耗在排查"AI 为什么这么做"而不是"让 AI 做得更好"上。
在设计一个 Agent 系统,先不要问"哪个框架最强大”,先问两个问题:出了问题,你能解释清楚它为什么这么做吗?预算耗尽了,有人喊停吗?
这两个问题的答案,决定你的架构选型。