语言: English · Tiếng Việt · 中文
OpenSpace 与 OpenViking 在运行时如何交互的端到端全景。
┌──────────────────────────────────────────────────────────────────────┐
│ Host Agent(OpenClaw、Claude Code、Codex、…) │
│ │
│ 路径 A: execute_task(instruction) ─── 委派任务 │
│ 路径 B: openviking_* MCP 工具 ──── 直接访问 Viking(新) │
└──────┬─────────────────────────────────────────┬──────────────────────┘
│ │
▼ ▼
┌─────────────────────────────────────┐ ┌──────────────────────────┐
│ OpenSpace(保持不变) │ │ mcp_tools.py(新) │
│ │ │ │
│ ┌──────────┐ ┌──────────────┐ │ │ 暴露给 host agent 的 │
│ │ Skill │ │ Grounding │ ◀─┐ │ │ 5 个 MCP 工具: │
│ │ Registry │▶│ Agent │ │ │ │ retrieve_memory │
│ └──────────┘ └──────────────┘ │ │ │ remember │
│ ▲ ▲ │ │ │ forget_memory │
│ │ hints │ iter-1 ctx │ │ │ report_stale_memory │
│ │ │ + mid-iter │ │ │ memory_status │
│ │ │ retrieve │ │ └────────────┬─────────────┘
│ │ │ _memory │ │ │
│ ┌────┴────────────┴────────┐ │ │ │
│ │ Execution Analyzer │ │ │ │
│ └────────────┬─────────────┘ │ │ │
│ │ evolved skills │ │ │
│ ┌────────────▼─────────────┐ │ │ │
│ │ Skill Evolver │ │ │ │
│ └────────────┬─────────────┘ │ │ │
│ │ │ │ │
└───────────────┼──────────────────┼──┼────────────────┼─────────────┘
│ │ │ │
┌───────────────┴──────────────────┴──┴────────────────┴─────────────┐
│ openspace/viking/ │
│ │
│ ┌────────────────┐ ┌──────────────────┐ ┌────────────────────┐ │
│ │ OpenVikingClient│ │ VikingEnrichment │ │ RetrieveMemoryTool │ │
│ │ • HTTP + URIs │ │ • compose_query │ │ • 迭代中使用的 │ │
│ │ • health cache │◀─│ • enrich_pre │ │ LocalTool │ │
│ │ • score_thresh │ │ • feedback_evo │ │ │ │
│ │ • delete res │ │ • feedback_neg │ │ │ │
│ │ • find_antipat │ │ • report_stale │ └────────────────────┘ │
│ └────────────────┘ │ • push_skills │ │
│ ┌────────────────┐ │ • env fingerprint│ ┌────────────────────┐ │
│ │ scrubber.py │─▶│ (应用 scrub) │ │ config.py │ │
│ │ • 正则 PII │ └──────────────────┘ │ • 身份解析 │ │
│ │ • Luhn CC 校验 │ │ • push/scrub/score │ │
│ │ • 私钥 │ │ • stats │ │
│ └────────────────┘ └────────────────────┘ │
└──────────────────────┬───────────────────────────────────────────────┘
│ HTTP(5s 超时,graceful fail)
▼
┌──────────────────────────────────────────────────────────────────────┐
│ OpenViking server(localhost:1933) │
│ │
│ /api/v1/search/find —— L0 abstracts(带 score_threshold) │
│ /api/v1/sessions —— 丰富的反馈 session(positive + negative) │
│ /api/v1/skills —— 结构化 skill 资源推送 │
│ /api/v1/resources —— DELETE 用于 forget_memory │
│ /health —— 存活探针 │
│ │
│ 后台:记忆抽取(8 类,包括 antipatterns)+ 向量化 │
└──────────────────────────────────────────────────────────────────────┘
| 文件 | 角色 |
|---|---|
openspace/viking/client.py |
OpenVikingClient —— 异步 HTTP、URI 构建器、二级健康缓存、skill push、find_antipatterns、score threshold、delete_resource |
openspace/viking/enrichment.py |
VikingEnrichment —— 执行前(6 个并行 find)、分析阶段、丰富反馈、negative feedback、skill push、staleness 上报、环境指纹 |
openspace/viking/config.py |
resolve_viking_identity、resolve_viking_push_enabled、resolve_viking_min_score、resolve_viking_scrub_pii、VikingExecutionStats(零依赖) |
openspace/viking/scrubber.py |
[新] 基于正则的 PII / 密钥清理器 —— API keys、JWT、邮箱、电话、信用卡(Luhn)、私钥 |
openspace/viking/mcp_tools.py |
[新] 暴露给 host agent 的 5 个 MCP 工具:retrieve_memory、remember、forget_memory、report_stale_memory、memory_status |
openspace/viking/retrieve_memory_tool.py |
[新] RetrieveMemoryTool —— 作为 LocalTool 供 grounding-agent 迭代中使用 |
openspace/viking/__init__.py |
Public API 导出 |
openspace/tool_layer.py |
配置字段、client 初始化、执行前 enrichment、反馈接线(positive + negative)、遥测、provide_feedback() 公共 API |
openspace/agents/grounding_agent.py |
_viking_context 属性、iter-2 剥离、_viking_client 属性、迭代中 RetrieveMemoryTool 注册 |
openspace/skill_engine/registry.py |
select_skills_with_llm 接受 cross_session_hints |
openspace/skill_engine/analyzer.py |
注入 viking_client、分析 prompt 增强、遥测属性 |
openspace/mcp_server.py |
_maybe_register_viking_tools() 在启动时自动注册 5 个 host MCP 工具 |
User(新)→ OpenSpace.execute("分析 sales.xlsx 并生成 dashboard")
│
├─ Viking /health: 200(或超时 → 空增强,流程继续)
├─ Viking /search/find × 5 并行(tool/pattern/skill/pref/case)
│ → 全部返回 [](还没学到任何东西)
├─ viking_context = "" → 跳过注入(无开销)
├─ Skill-first Phase 1(和今天一样)
├─ 执行... 12 次 iteration, 45 次工具调用
├─ Analyzer 运行 → evolution_suggestions: [DERIVED(sales-dashboard-builder)]
├─ Evolver 在本地创建新 skill
├─ Viking 丰富反馈 session(task + 响应 + 工具序列 + skill 元数据)
│ → commit → Viking 异步抽取
│ • 抽取到 viking://.../agent/memories/skills/
│ • 从 task 文本抽取用户偏好到 user/memories/preferences/
│ • 抽取工具知识(shell:xlsx_to_csv 成功)
├─ Viking skill 资源推送(如果 OPENVIKING_PUSH_SKILLS=true)
└─ 响应 → user,遥测数据一并返回
结果:这个 task 从 Viking 得到零收益。Baseline 成本 = 和今天完全一样。开销:约 5 次失败 HTTP 调用 = 几毫秒(有超时保护)。这里创建的记忆将在未来的 task 中变得可用。
User → OpenSpace.execute("生成本周销售的 dashboard")
│
├─ compose_query = task + 最后 3 个用户轮次(来自 conversation_history)
├─ Viking /search/find × 5 并行:
│ • find_user_preferences → "用户偏好柱状图,保存 XLSX 而非 CSV"
│ • find_tool_knowledge → "shell:xlsx_to_csv 对 .xlsx 有效,对 .xlsm 失败"
│ • find_skill_knowledge → "sales-dashboard-builder 用 8 次 iteration 解决过类似的"
│ • find_cases → "过往 task 在聚合前清理了 NaN 行"
│
├─ 构建两个增强块:
│ 1. context_injection(~800 tokens,完整 L0 abstract)
│ 2. selector_hints(~150 tokens,仅用户偏好 + 顶级 skill/case)
│
├─ _select_and_inject_skills(task, cross_session_hints=selector_hints)
│ → Selector LLM 看到 hints 块 → 自信地选择 sales-dashboard-builder
│
├─ grounding_agent._viking_context = context_injection
├─ Phase 1 执行 → LLM 已经知道:
│ ✓ 用户想要柱状图 → 跳过澄清
│ ✓ 文件很可能是 .xlsx → 跳过 .xlsm 探测
│ ✓ NaN 处理 → 跳过 2 次调试 iteration
│ → 用 6 次 iteration 完成,而不是 12 次
│
├─ iter 2+:viking_context 和 skill_context 都被剥离(grounding_agent.py:232-251)
├─ Analyzer 运行 → 自信,无需建议 evolution
└─ 响应 → user
结果:iteration 数减少约 50%,这个 task 的 token 减少约 25–40%。详细计算见 Token 经济学。
Agent A(Claude Code 用户,周一 10am)
└─ OpenSpace → 修复 "chromedriver 版本不匹配" task
└─ Viking 抽取工具知识:
"chromedriver 124 需要 Chrome 124+"
→ viking://tenants/acme/agent/memories/tools/chromedriver.md
几小时后...
Agent B(Codex 用户,不同机器,周一 2pm)
└─ OpenSpace → "爬取 example.com"
└─ Viking find_tool_knowledge("chromedriver 设置")
→ 返回 [{abstract: "chromedriver 124 需要 Chrome 124+", score: 0.88}]
└─ Agent B 跳过整个 3 次 iteration 的调试循环
关键洞察:两个 Agent 连接到同一个团队的 Viking 服务器(相同的 OPENVIKING_NAMESPACE)。Agent A 的 session commit 自动触发记忆抽取;Agent B 之后的 find_tool_knowledge 调用自然而然地检索到它。没有手工 upload 步骤,没有显式的"分享"按钮。
这是 OpenSpace 公开宣传的 "一个 Agent 学会,所有 Agent 受益" 口号从一个向往中的云端 upload workflow 变成了一个自动的跨 Agent 反馈闭环。
Task 在 iter 15 失败 —— analyzer 启动
│
├─ analyzer._build_analysis_prompt(context) [async]
│ ├─ 构建正常 prompt(traj + skill 内容 + 对话)
│ ├─ 检查 self._viking_client(OpenSpace 初始化时共享 —— 没有新 HTTP 连接)
│ ├─ 从 traj_records 中提取 status=error 的 tool_issues
│ ├─ enricher.enrich_analysis_context(task, tool_issues):
│ │ • find_tool_knowledge(task) —— 通用工具模式
│ │ • find_cases(task) —— 历史解决方案
│ │ • find_tool_knowledge(tool_issues[0]) —— 特定的已知修复
│ ├─ 追加格式化 context 到 resource_info
│ └─ 设置 self._last_viking_context_chars(遥测)
│
├─ 分析 LLM 现在知道 "这个错误模式 → 上周应用过的解决方案 X"
│ → 发出 evolution_type=FIX(精准编辑)
│ → FIX 比 CAPTURED(从头新建 skill)便宜
│
└─ Evolver 处理建议
为什么这很重要:Analyzer 通常是 OpenSpace task 中最贵的 LLM 调用(来自 traj + 对话 + skill 内容的 30k+ input tokens)。一个小小的增强(额外 ~500 tokens)就能把推测性的"新建 skill"建议转化为精准的两行修复,节省整个后续 evolution 周期。
OpenSpace.execute(task, context={conversation_history, ...})
│
├── [if viking_client && is_available()]
│ VikingEnrichment.enrich_pre_execution(task, conversation_history)
│ │
│ ├── compose_query(task, history)
│ │ • 提取最后 3 个 USER 轮次(multimodal 仅提取文本部分)
│ │ • 用 "Prior user turns: ... || ..." 连接
│ │ • 截断到 500 字符
│ │
│ ├── asyncio.gather( ── 6 个并行 FINDS(之前是 5 个)──
│ │ find_tool_knowledge(query, limit=5, score_threshold=min),
│ │ find_pattern_knowledge(query, limit=5, score_threshold=min),
│ │ find_skill_knowledge(query, limit=5, score_threshold=min),
│ │ find_user_preferences(query, limit=3, score_threshold=min),
│ │ find_cases(query, limit=3, score_threshold=min),
│ │ find_antipatterns(query, limit=3, score_threshold=min), ← 新增
│ │ )
│ │ → 6 个并行 HTTP POST,每个 5s 超时
│ │ → 每个都带作用域化的 target_uri 命中 /api/v1/search/find
│ │ 以及 payload 中的 score_threshold
│ │ → 客户端安全网会 drop 任何低于 score_threshold 的命中
│ │
│ ├── 去重 abstract,构建两个输出块:
│ │ 1. context_injection(markdown 5 节,包括
│ │ "## Known Failure Modes (AVOID)" 用于 anti-patterns)
│ │ 2. selector_hints(更短 —— 偏好 + 顶级 skill + 顶级 case
│ │ + "AVOID (prior failures)" 条目)
│ │
│ └── return {context_injection, selector_hints, hit_counts,
│ antipattern_hints, query, ...}
│
├── _viking_stats.query / hit_counts / enrichment_chars 填充
├── grounding_agent._viking_context = context_injection
│
└── _select_and_inject_skills(task, cross_session_hints=selector_hints)
└── registry.select_skills_with_llm(task, ..., cross_session_hints=...)
└── prompt 包含 "# Cross-Session Hints (non-authoritative)"
Latency 预算:6 个并行 HTTP 调用,每个 5s 超时。典型缓存响应:50–200ms(并行意味着 6 个 find 成本约等于 1 个)。最坏情况:超时 → [] → 空 context → 零 prompt 开销。
注入的 token 成本:6 个类别 × 约 3 条 abstract × 约 100 tokens ≈ 1,800 tokens,仅 iter 1。由 grounding_agent.py 从 iter 2 起剥离。
质量过滤:当设置 OPENVIKING_MIN_SCORE=0.5(或 openviking_min_score 配置字段)时,低置信度命中会在服务器端 score_threshold 参数和 find_memories() 中的客户端安全网两处都被丢弃。防止错误的记忆进入 prompt。
OpenSpace.execute() → finally: _maybe_analyze_execution(task_id, ...)
│
├── analyzer.analyze_execution() → 带 evolution_suggestions 的分析
│ └── _build_analysis_prompt() [ASYNC]
│ └── 使用 self._viking_client(共享,不是新连接)
│ └── enrich_analysis_context → resource_info 块
│ └── self._last_viking_context_chars = N(遥测)
│
├── _viking_stats.analysis_context_used = (chars > 0)
│
├── evolver.process_analysis() → evolved_records
│
├── scrub_enabled = resolve_viking_scrub_pii(config.openviking_scrub_pii)
│
├── [POSITIVE PATH —— if evolved && viking_client]
│ │
│ ├── 为每个 evolved record 从磁盘读取 SKILL.md 内容
│ ├── 从 execution_result.tool_executions 提取 tool_sequence(前 30 个)
│ ├── 从 grounding_agent._current_instruction 提取 task_description
│ ├── 从 execution_result.response 提取 final_response
│ │
│ ├── VikingEnrichment.feedback_evolution(
│ │ task_id,
│ │ feedback_data,
│ │ task_description=...,
│ │ final_response=...,
│ │ tool_sequence=...,
│ │ scrub_pii=scrub_enabled, ← 新:应用 scrubber.py
│ │ )
│ │ │
│ │ ├── clean = _make_scrub(scrub_enabled) — 正则 PII redactor
│ │ ├── create_session("openspace-evo-<task_id>")
│ │ ├── add_session_message("user", clean(task_description))
│ │ ├── add_session_message("assistant", clean(f"Final response:..."))
│ │ ├── add_session_message("assistant", clean("Tool sequence: ..."))
│ │ ├── add_session_message("assistant",
│ │ │ f"Environment: {_env_fingerprint()}") ← 新增
│ │ ├── add_session_message("assistant", clean(f"Skill 'X' evolved...")) # × N
│ │ └── commit_session(...)
│ │
│ ├── _viking_stats.feedback_status = "committed"
│ │
│ └── [if resolve_viking_push_enabled()]
│ push_evolved_skills(feedback_data, scrub_pii=scrub_enabled)
│ → 每个 skill content 在 POST /api/v1/skills 前经过 scrubber
│ → metadata 包含 env fingerprint
│ _viking_stats.pushed_skills = N
│
└── [NEGATIVE PATH —— 新 —— if status in (error, incomplete) && viking_client]
│
├── failure_reason = execution_result.error or response
├── tool_sequence = execution_result.tool_executions[:30]
│
└── VikingEnrichment.feedback_negative(
task_id,
task_description=...,
failure_reason=...,
tool_sequence=...,
scrub_pii=scrub_enabled,
)
│
├── create_session("openspace-neg-<task_id>")
├── add_session_message("assistant",
│ "POLARITY: negative — 这是一个失败的执行记录...")
├── add_session_message("user", clean(task_description))
├── add_session_message("assistant", clean(f"Failure reason: ..."))
├── add_session_message("assistant", clean(f"Tool sequence: ..."))
├── add_session_message("assistant",
│ f"Environment: {_env_fingerprint()}")
└── commit_session(...)
→ Viking 抽取到 viking://.../agent/memories/antipatterns/
→ 未来的 task 会看到它作为 "AVOID" 警告
隐私防御:当 OPENVIKING_SCRUB_PII=true(默认)时,每个用户编写的字符串在到达 Viking 之前都会通过正则清理器。API keys、JWT、邮箱、电话、Luhn 校验通过的信用卡和私钥块都会被替换为 [REDACTED_*] 占位符。幂等 —— 对已清理的文本再次清理是 no-op。
OpenSpace 侧成本:每个 evolved skill 约 4–6 次 HTTP 调用(session 创建 + 3 条消息 + commit + 可选资源推送)。对用户感知的延迟零影响 —— 在 execute() 已经有返回值之后的 finally 块中运行。
OpenViking 把每个上下文存储在三层:
| 层 | 大小 | 何时获取 |
|---|---|---|
| L0 abstract | ~100 tokens | /api/v1/search/find 默认返回 |
| L1 overview | ~1–2k tokens | 按需通过 /api/v1/resources/overview |
| L2 detail | 无限 | 按需通过 /api/v1/resources/read |
我们目前的集成只获取 L0。28 条 abstract(6 类 × ~5 limit)× 100 tokens = 最坏情况 ~2.8k tokens;经过去重和截断后通常是 800–1,800 tokens。这是有意的保守。未来的增强可以在置信度超过阈值时为评分最高的 top-1 case 获取 L1,但目前没有接入。
grounding_agent.process() → iteration loop
│
│ iter 1: 获得执行前 enrichment(D1 路径)
│ iter 2: skill_context 和 viking_context 都被剥离
│ iter 3+: LLM 可能发现它需要不同的知识
│
▼
LLM 发出 tool call → retrieve_memory(query="error X", category="antipatterns")
│
▼
RetrieveMemoryTool._arun(query, category, limit=5)
│
├── is_available() 检查(cached)
├── 规范化 category → target_uri 通过 client.agent_memory_uri(...)
│ 或 client.user_memory_uri(...)
├── find_memories(query, target_uri, limit, score_threshold)
│ → 1 个 HTTP POST 到 /api/v1/search/find
│
└── 格式化结果为纯文本块:
"# Retrieved from OpenViking — query: 'error X'
# Category: antipatterns, 3 result(s)
## WARNING: these are FAILED approaches...
1. [score=0.82] chromedriver 124 fails on macOS <14
uri: viking://.../antipatterns/...
2. ..."
│
▼
LLM 收到 tool result → 决定下一步行动
→ 可能避免失败的方法
→ 可能用不同的 category 再次调用 retrieve_memory
→ 可能采用替代策略
为什么这很重要:执行前 enrichment 只能看到初始查询。如果 agent 在执行中发现真正的问题与查询所暗示的不同,D4 让它可以拉取新鲜知识而无需等待下一个 task。该工具在 grounding_agent.py 中与 retrieve_skill 并排注册,条件是 openviking_mid_iter_tool=True 且 _viking_client 已设置。
成本:每次调用 1 个 HTTP。Agent 只在真正卡住时调用 —— 每个 task 通常 0–2 次。相对于替代方案(可能需要 3+ 次 iteration 的探索循环),开销可忽略。
Host agent 的 chat LLM(例如 OpenClaw Claude 或 Codex)
│
│ 用户输入: "我之前说过 dashboard 颜色偏好是什么来着?"
│
▼
Host LLM 看到 MCP tool 列表 → 选择 openviking_retrieve_memory
│
▼
MCP 服务器把调用路由到 tool_retrieve_memory(get_client, query, category, limit)
│
├── get_client() → _get_viking_client()
│ → lazy-init OpenSpace → return os._viking_client
│
├── 验证:非空 query、is_available() 为 True
├── _resolve_target_uri(client, category="preferences")
│ → viking://tenants/<ns>/user/<uid>/memories/preferences/
│
├── client.find_memories(query, target_uri, limit)
│
└── 返回 JSON 给 MCP 客户端:
{
"status": "ok",
"data": {
"query": "...",
"category": "preferences",
"target_uri": "viking://...",
"count": 2,
"results": [
{"uri": "...", "abstract": "prefers teal accent, white background",
"score": 0.91, "category": "preferences"},
...
]
}
}
│
▼
Host LLM 接收结果 → 用它来个性化聊天回复
→ 不需要 OpenSpace execute_task() 调用
→ 用户立即得到个性化响应
这是 Round 6 最大的架构改进。 之前的设计要求通过 OpenSpace 进行完整的 execute_task() 往返,host 才能从 Viking 记忆中受益。现在 host 可以直接 retrieve(以及 write、forget、report stale)记忆 —— 使跨会话个性化对每个支持 MCP 的 host agent 普遍可用。
由 register_viking_mcp_tools(mcp, _get_viking_client) 注册的 5 个工具:
| 工具 | 用途 | 典型调用方 |
|---|---|---|
openviking_retrieve_memory(query, category?, limit?) |
获取 L0 abstracts | Host LLM 个性化回复 |
openviking_remember(content, category, polarity?) |
写入新记忆 | Host LLM 捕获用户明确陈述 |
openviking_forget_memory(uri, reason?) |
删除 / 弃用 | Host LLM 响应"忘了这个" |
openviking_report_stale_memory(uri, reason) |
标记为过时(比 forget 轻) | Host LLM 注意到过时记忆 |
openviking_memory_status() |
健康 + 配置自省 | Host LLM 在 UI 中显示集成状态 |
所有工具都返回结构化 JSON({"status": "ok"|"error", ...}),以便 host LLM 能够优雅地处理 Viking 不可用的情况。
grounding agent 迭代循环强制严格的 context 卫生,让每次 iteration 的 token 成本保持常数:
# openspace/agents/grounding_agent.py:226-251
while current_iteration < max_iterations:
current_iteration += 1
# 在 iteration 2 剥离 skill context 和 viking context。
# 两者都是规划提示 —— 对构建执行计划的第一次 LLM 调用有用,
# 一旦工具结果开始在消息历史中累积,它们就变成纯粹的噪音。
if current_iteration == 2:
drop_contents = set()
if self._skill_context:
drop_contents.add(self._skill_context)
viking_ctx = getattr(self, "_viking_context", "")
if viking_ctx and viking_ctx.strip():
drop_contents.add(viking_ctx)
if drop_contents:
messages = [
m for m in messages
if not (m.get("role") == "system" and m.get("content") in drop_contents)
]
# 每次 iteration 的上限和截断也适用
if current_iteration >= 2:
messages = cap_message_content(messages)
if current_iteration >= 5:
messages = truncate_messages(messages, keep_recent=8, max_tokens_estimate=120000)没有 iter-2 剥离,每次 iteration 都会把 1,500 tokens 的 Viking 块重新发给 LLM。一个 12 次 iteration 的 task 会在陈旧提示上浪费 ~18k tokens。剥离是 Viking 集成中最重要的 token 节省机制。
当 OPENVIKING_NAMESPACE=acme 且 OPENVIKING_USER_ID=alice 时,client 构建:
Agent 共享(团队级):
viking://tenants/acme/agent/memories/tools/
viking://tenants/acme/agent/memories/patterns/
viking://tenants/acme/agent/memories/skills/
viking://tenants/acme/agent/memories/cases/
用户隔离(团队内按用户):
viking://tenants/acme/user/alice/memories/preferences/
单租户开发机(无 namespace、无 user_id 设置,回退到 $USER=jimmy):
Agent 共享(全局):
viking://agent/memories/tools/
用户隔离(全局用户目录):
viking://user/memories/preferences/
完整的解析链和部署模式见 配置。
OpenSpace.execute(task)
│
├─[viking_enabled=False]────────────────────▶ 没有 Viking 代码运行,never。
│
├─[httpx 未安装]────────────────────────────▶ _get_client() 返回 None,
│ 所有方法返回 {}/[]
│
├─[服务器不可达]────────────────────────────▶ 5s 超时 → 异常 →
│ 捕获 → 返回 {}/[]
│
├─[服务器返回 4xx/5xx]──────────────────────▶ 状态检查 → None →
│ 返回 {}/[]
│
├─[服务器返回 malformed JSON]───────────────▶ json.loads 异常 →
│ 返回 {}
│
├─[find 返回意外格式]───────────────────────▶ 解析器尝试多种包络
│ → 返回 []
│
├─[添加消息后 commit 失败]──────────────────▶ 异常捕获,
│ logger.warning,不 raise
│
└─[某个 record 的 skill push 失败]──────────▶ 每 record try/except,
pushed 计数只反映
成功
每条路径都通向 graceful degradation。面向用户的契约是:如果 Viking 坏了,OpenSpace 运行时就像 Viking 不存在一样。
下一页:Token 经济学 用真实 baseline 量化实际节省。