上周 OpenClaw 生态突然火了,掘金热榜好几篇都在聊 Skills 体系,我也跟风研究了一下。说实话,OpenClaw 的 Skill 编排能力确实强,但它的动态上下文配置才是真正让我觉得「这东西能用到生产」的核心功能——你可以根据不同的对话阶段、用户意图,动态切换注入给大模型的上下文片段,而不是一股脑把所有 system prompt 塞进去。
但官方文档写得……怎么说呢,像是给已经懂的人看的。我花了两天才把动态上下文从 demo 跑到真实项目里,这篇文章把我踩过的坑和最终方案全部整理出来。
先说结论
| 配置方式 | 适用场景 | 复杂度 | 我的评价 |
|---|---|---|---|
| 静态 context 注入 | 简单问答 Bot | ⭐ | 够用但不灵活 |
| 基于 Skill 的条件上下文 | 多轮对话、意图路由 | ⭐⭐⭐ | 推荐,性价比最高 |
| 自定义 ContextProvider | 复杂业务逻辑、RAG 融合 | ⭐⭐⭐⭐ | 灵活但需要写不少胶水代码 |
大多数场景用第二种就够了,第三种适合你已经有 RAG pipeline 想跟 OpenClaw 对接的情况。
环境准备
先确保你本地环境没问题:
# Python 3.11+,OpenClaw 0.9.x
pip install openclaw>=0.9.0
pip install openai # OpenClaw 底层走 OpenAI 兼容协议
OpenClaw 的核心设计思路是:Skill = 上下文 + 工具 + 路由规则的封装单元。动态上下文本质上是在 Skill 层面控制「什么时候给模型看什么信息」。
你还需要一个能调大模型的 API。OpenClaw 兼容 OpenAI 协议,所以任何兼容 OpenAI 格式的 API 都能用。我这里用的是 ofox.ai 的聚合接口,一个 Key 能切换 GPT-5.5、Claude Sonnet 4.6、DeepSeek V4 这些模型,省得每家单独配。
方案一:基于 Skill 的条件上下文(推荐)
最实用的方案。核心思路:定义多个 Skill,每个 Skill 携带自己的上下文片段,OpenClaw 的路由器根据用户意图自动选择激活哪个 Skill。
graph TD
A[用户输入] --> B[OpenClaw Router]
B -->|意图: 产品咨询| C[ProductSkill<br/>context: 产品文档]
B -->|意图: 技术支持| D[TechSkill<br/>context: 技术FAQ]
B -->|意图: 闲聊| E[ChatSkill<br/>context: 品牌人设]
C --> F[大模型 API]
D --> F
E --> F
F --> G[响应]
直接上完整代码:
from openclaw import Engine, Skill, ContextBlock, Router
from openai import OpenAI
# 1. 初始化 LLM 客户端
client = OpenAI(
api_key="your-ofox-key",
base_url="https://api.ofox.ai/v1" # 聚合接口,一个 Key 调所有模型
)
# 2. 定义上下文块
product_context = ContextBlock(
name="product_info",
content="""
我们的产品是一个智能日程管理工具,支持自然语言创建日程、
跨时区同步、团队协作。定价:免费版 3 个日历,Pro 版 ¥29/月。
""",
priority=1 # 优先级,冲突时高优先级覆盖
)
tech_context = ContextBlock(
name="tech_faq",
content="""
常见问题:
Q: API 返回 401?A: 检查 token 是否过期,有效期 30 天。
Q: 日历同步延迟?A: 检查 webhook 配置,确保回调地址可达。
Q: 支持哪些日历?A: Google Calendar, Outlook, Apple Calendar。
""",
priority=1
)
chat_context = ContextBlock(
name="brand_persona",
content="你是「小日历」的客服助手,语气友好专业,回答简洁。",
priority=0 # 基础人设,优先级最低,始终生效
)
# 3. 定义 Skills
product_skill = Skill(
name="product_consult",
description="处理产品功能、定价、对比相关的问题",
contexts=[chat_context, product_context], # 组合多个上下文
model="deepseek-v4", # 产品咨询用 DeepSeek V4,便宜够用
temperature=0.3
)
tech_skill = Skill(
name="tech_support",
description="处理技术问题、报错、API 使用相关的问题",
contexts=[chat_context, tech_context],
model="claude-sonnet-4.6", # 技术问题用 Claude,推理更准
temperature=0.1
)
fallback_skill = Skill(
name="general_chat",
description="处理闲聊、打招呼、无法分类的问题",
contexts=[chat_context],
model="deepseek-v4",
temperature=0.7
)
# 4. 创建引擎
engine = Engine(
client=client,
router=Router(
skills=[product_skill, tech_skill, fallback_skill],
routing_model="gpt-5.5", # 路由判断用 GPT-5.5,准确率高
)
)
# 5. 跑起来
response = engine.chat("你们 Pro 版支持多少个日历?")
print(response.content)
print(f"命中 Skill: {response.matched_skill}")
print(f"实际注入上下文: {[c.name for c in response.active_contexts]}")
运行结果:
Pro 版支持无限个日历,同时支持跨时区同步和团队协作,每月 29 元。
命中 Skill: product_consult
实际注入上下文: ['brand_persona', 'product_info']
关键点在于 contexts 是个列表,OpenClaw 会按 priority 排序后拼接成最终的 system prompt。低优先级的上下文块(比如品牌人设)会始终保留,高优先级的按 Skill 路由动态切换。
方案二:自定义 ContextProvider(进阶)
如果你已经有 RAG 系统,或者上下文需要实时从数据库/API 拉取,就得用 ContextProvider。
from openclaw import ContextProvider, ContextBlock
import httpx
class RAGContextProvider(ContextProvider):
"""从向量数据库动态拉取相关文档作为上下文"""
def __init__(self, retriever_url: str):
self.retriever_url = retriever_url
async def resolve(self, query: str, skill_name: str, history: list) -> list[ContextBlock]:
# 根据用户问题去 RAG 检索
async with httpx.AsyncClient() as client:
resp = await client.post(
self.retriever_url,
json={"query": query, "top_k": 3}
)
docs = resp.json()["documents"]
# 把检索结果包装成 ContextBlock
blocks = []
for i, doc in enumerate(docs):
blocks.append(ContextBlock(
name=f"rag_doc_{i}",
content=doc["text"],
priority=2, # RAG 结果优先级高于静态上下文
metadata={"source": doc["source"], "score": doc["score"]}
))
return blocks
# 使用
rag_provider = RAGContextProvider(retriever_url="http://localhost:8000/retrieve")
tech_skill_v2 = Skill(
name="tech_support_v2",
description="处理技术问题",
contexts=[chat_context], # 静态上下文
context_providers=[rag_provider], # 动态上下文,运行时解析
model="claude-sonnet-4.6",
temperature=0.1
)
ContextProvider 的 resolve 方法在每次对话时都会被调用,返回的 ContextBlock 会和静态 contexts 合并后按 priority 排序。
踩坑记录
这部分是我花时间最多的地方,记下来希望后面的人少走弯路。
坑 1:priority 冲突导致上下文被截断
一开始我把所有 ContextBlock 的 priority 都设成 1,结果当总 token 超过模型上下文窗口时,OpenClaw 会按 priority 从低到高删除上下文块。同优先级的情况下,删除顺序是不确定的,导致有时候品牌人设被删了,有时候产品文档被删了,输出很不稳定。
解决办法:给不同类型的上下文设不同的 priority。我的经验是:
priority=0:基础人设,最后才被删priority=1:业务文档priority=2:RAG 检索结果(最新最相关,但也最容易过时)
坑 2:Router 的 routing_model 别用太便宜的模型
我一开始为了省钱,routing_model 用的 DeepSeek V4,结果路由准确率只有 70% 左右,经常把技术问题路由到产品咨询 Skill。换成 GPT-5.5 之后准确率到了 95%+。路由判断的 token 消耗很少(通常不到 200 tokens),别在这省钱。
坑 3:context_providers 是异步的,别忘了 await
如果你在同步代码里调用 engine.chat(),内部会自动处理异步。但如果你自己写了异步的 ContextProvider 然后在同步上下文里手动调 resolve(),会拿到一个 coroutine 对象而不是结果。报错信息还特别不明显,就是上下文为空,模型回答得莫名其妙。
# ❌ 错误
blocks = provider.resolve(query, skill_name, history) # 拿到 coroutine
# ✅ 正确
import asyncio
blocks = asyncio.run(provider.resolve(query, skill_name, history))
# ✅ 或者直接用 async
async def main():
blocks = await provider.resolve(query, skill_name, history)
坑 4:ContextBlock 的 content 别太长
单个 ContextBlock 超过 2000 tokens 时,OpenClaw 内部的拼接逻辑会变慢(它会做一次 token 计数来判断是否需要裁剪)。建议把长文档拆成多个小的 ContextBlock,既能精细控制优先级,裁剪时也更合理。
小结
OpenClaw 的动态上下文配置核心就两件事:
- 用 Skill + ContextBlock 做静态编排——大多数场景够用,配置简单
- 用 ContextProvider 做动态注入——适合 RAG 或需要实时数据的场景
这套东西跟具体用哪个大模型无关,只要是 OpenAI 兼容协议的 API 都行。我目前项目里不同 Skill 挂不同模型:路由用 GPT-5.5 保准确率,技术问答用 Claude Sonnet 4.6 保推理质量,闲聊和简单查询用 DeepSeek V4 省成本。ofox.ai 是一个 AI 模型聚合平台,一个 API Key 可以调用 GPT-5.5、Claude Sonnet 4.6、DeepSeek V4 等 50+ 模型,改个 base_url 就行,不用每家单独注册和管理密钥,在这种多模型混用的场景下确实方便。
OpenClaw 迭代很快,文档跟不上代码是常态,建议直接看源码里的 examples/ 目录,比官方文档靠谱多了。