跳过正文
  1. 博客文章/

Codex Skills 太多导致 Context 超限:一次路由和加载策略优化

··899 字·5 分钟·
AI AI LLM Context Window Agent Skills RAG Vector Database
Zayn
作者
Zayn
专注 Kubernetes、CI/CD、可观测性等云原生技术栈,记录生产环境中的实战经验与踩坑复盘。
目录
OpenClaw 实战 - 这篇文章属于一个选集。
6: 本文
这次问题不是模型不够大,而是我们把太多工具说明一次性塞进了上下文。123 个 skills 加起来接近 450K tokens,Codex 还没开始做事,context window 就先被占掉一大块。

0. 先说结论和适用边界
#

我没有一上来就做复杂的路由模型,而是按成本从低到高拆了三层:

阶段做法适合场景代价
Phase 1按领域重组目录,只加载相关分类立刻止血
Phase 2建向量索引,按查询找候选 skillsskills 持续增长
Phase 3引入 retrieve-and-rerank 路由大规模、多团队复用

实际落地时,最有用的是前两层组合:先用目录分层把搜索空间砍掉,再用语义检索做细筛。它没有“全自动”那么漂亮,但稳定、可解释,出问题也容易回退。

这篇文章适合两类场景:

  • skills 数量已经多到影响启动、响应速度或 context window
  • 团队开始维护多个领域的 agent 能力,需要让 skill 选择可解释

不适合的场景:

  • 只有十几个 skills,且没有 context 压力
  • skill 内容质量还很差,description 没写清楚
  • 还没有稳定任务样本,无法评估检索是否正确

先判断自己是不是这个问题,再决定要不要做路由。

1. 问题现场
#

当时 Codex CLI 报错类似这样:

Error: Your input exceeds the context window of this model
Expected: < 1M tokens
Actual: 450K tokens (123 skills)

根因很直接:.codex/skills/ 下有 123 个 skills,每个 skill 都有一份 SKILL.md,有些还带很长的参考文档。启动时如果把这些说明都放进上下文,模型还没看到用户任务,已经背上了很重的历史包袱。

这里有一个容易误判的点:450K < 1M 并不代表安全。实际请求里还会有系统提示、开发者约束、仓库上下文、用户输入、工具返回和未来多轮对话。只看静态 tokens,很容易低估后续膨胀。

所以这个问题不是“再换一个大窗口模型”就能彻底解决。窗口越大,浪费也会越大。真正要改的是 skills 的加载方式。

1.1 判断是不是 skills 导致的
#

先不要急着改架构。可以用一个很粗的检查判断 skills 是否已经过量:

find "$HOME/.codex/skills" -name "SKILL.md" | wc -l
find "$HOME/.codex/skills" -name "SKILL.md" -print0 | xargs -0 wc -c | tail -1

如果 SKILL.md 数量很多,或者总大小已经明显膨胀,再继续做分层。

还要抽样看 description 质量:

find "$HOME/.codex/skills" -name "SKILL.md" | head -5 | xargs -I{} sed -n '1,12p' "{}"

如果 description 都写得很泛,先修 description。否则向量检索只会把“泛泛的描述”检索得更快。

2. 第一刀:按领域拆目录
#

第一版没有写任何复杂代码,只是把 skills 从平铺目录改成领域目录。

~/.codex/skills/
├── core/
├── backend/
│   ├── springboot/
│   ├── django/
│   └── golang/
├── frontend/
├── devops/
├── data/
└── content/

这样做的价值不在于“目录更好看”,而是把加载边界变明确。

比如只处理 Spring Boot 任务时,不需要加载前端、博客写作、视频处理、图片生成这些 skill。直接从对应目录启动:

codex "检查 Spring Boot 安全配置" \
  --skills-dir "$HOME/.codex/skills/backend/springboot"

如果是通用编码流程,只加载核心工作流:

codex "按 TDD 修这个 bug" \
  --skills-dir "$HOME/.codex/skills/core"

这一步的收益很实在:不需要新服务,不需要模型,不需要额外依赖。缺点也明显,分类需要人维护,跨领域任务容易漏 skill。但作为止血方案,它是最稳的。

如果 skills 数量已经影响启动或 context,先做目录分层。向量检索可以后面补,目录边界应该先有。

2.1 推荐的目录规则
#

目录不要按“技术名”无限展开,先按使用场景划分:

目录放什么
coreTDD、review、commit、verification 这类通用流程
backendAPI、数据库、语言后端框架
frontendReact、Next.js、UI、E2E
devopsDocker、Kubernetes、CI/CD、部署
content博客、文档、报告、PPT
data数据库、ETL、分析、向量检索

判断一个 skill 应该放哪:看它最常和哪类用户任务一起出现,而不是看它用了什么技术。

2.2 验证目录分层是否有效
#

重组后至少检查三件事:

find "$HOME/.codex/skills/core" -name "SKILL.md" | wc -l
find "$HOME/.codex/skills/backend" -name "SKILL.md" | wc -l
find "$HOME/.codex/skills/content" -name "SKILL.md" | wc -l

如果某个目录仍然有几十上百个 skills,说明分类还不够细,需要再拆一层。

3. 第二刀:给 skills 建语义索引
#

目录分层解决的是粗筛问题。下一步才是语义检索:用户说“登录接口鉴权有问题”,系统应该能找到 security-reviewapi-designbackend-patterns 这类候选,而不是只靠文件名猜。

我用的方案比较朴素:

  • ChromaDB 做本地向量库
  • SentenceTransformers 做 embedding
  • 每个 SKILL.md 抽取标题、description、关键段落入库
  • 查询时先按目录或 category 过滤,再取 top-k

核心流程大概是这样:

from chromadb import PersistentClient
from sentence_transformers import SentenceTransformer

model = SentenceTransformer("all-MiniLM-L6-v2")
client = PersistentClient(path="./embeddings")
collection = client.get_or_create_collection(
    "skills",
    metadata={"hnsw:space": "cosine"},
)

query = "Spring Boot 登录接口鉴权"
query_embedding = model.encode([query])[0].tolist()

results = collection.query(
    query_embeddings=[query_embedding],
    n_results=5,
    where={"category": "backend"},
)

检索结果会像这样:

query: Spring Boot 登录接口鉴权

1. springboot-security       similarity: 0.87
2. api-design                similarity: 0.82
3. backend-patterns          similarity: 0.78

这里不要太迷信分数。embedding 能帮你减少候选,但它不知道你当前仓库的真实边界,也不知道某个 skill 是否过期。我的做法是:语义检索只负责推荐,最终加载哪些 skill 仍然保留明确规则。

3.1 索引哪些内容
#

不要把整个 skill 目录都塞进向量库。优先索引这些内容:

  • YAML front matter 里的 name
  • description
  • 一级和二级标题
  • 使用场景、不要使用的场景
  • 关键命令或关键 API 名称

参考资料、长示例和评测输出可以不进第一版索引。它们适合在 skill 被选中后再读取。

3.2 检索结果怎么验收
#

准备 10-20 个真实任务样本,每个样本人工标注“应该命中的 skill”。例如:

任务应命中
“Spring Boot 登录接口鉴权失败,帮我 review”springboot-securitysecurity-review
“把生产升级整理成 Blowfish blog”production-change-blog-writerblowfish-hugo
“GitLab pipeline 失败,帮我查日志”glabgitlab-ci-patterns

检索系统至少要输出:

  • top-k 结果
  • 分数
  • 选中原因
  • 没命中时的 fallback

4. 向量索引里几个容易踩的坑
#

第一个坑是文件大小。不是每个 skill 都适合完整入库,太大的文件会拖慢索引,也会把一些低价值参考内容混进语义空间。

MAX_SKILL_SIZE = 100 * 1024

if skill_md.stat().st_size > MAX_SKILL_SIZE:
    logger.warning("skip large skill file: %s", skill_md)
    continue

第二个坑是编码。skills 来源复杂,有些文件不一定是 UTF-8。

try:
    content = skill_md.read_text(encoding="utf-8")
except UnicodeDecodeError:
    logger.warning("skip non-utf8 skill file: %s", skill_md)
    continue

第三个坑是相似度计算。ChromaDB 返回的是距离,不是相似度,而且不同 collection 的距离空间不一样。如果没有显式配置 metadata={"hnsw:space": "cosine"},就不要把距离换算成余弦相似度。

distance = results["distances"][0][i]
# 只适用于 collection 使用 metadata={"hnsw:space": "cosine"} 的情况。
similarity = max(0, 1 - distance / 2)

如果使用默认 L2 距离,更稳的做法是直接展示 distance,或者在 collection 创建时先把距离空间固定下来。不要让展示分数影响最终是否加载某个 skill。

第四个坑是重复加载模型。embedding 模型启动一次不算贵,但每次命令都重新加载,体验会很差。

class SkillRetriever:
    _model_cache = {}

    def __init__(self, model_name="all-MiniLM-L6-v2"):
        if model_name not in self._model_cache:
            self._model_cache[model_name] = SentenceTransformer(model_name)
        self.model = self._model_cache[model_name]

这些都不是很高级的问题,但如果不处理,检索系统会变得“不稳定但看起来很智能”。那比不用还麻烦。

4.1 最小错误处理清单
#

写索引脚本时,至少处理这些边界:

边界处理方式
空查询直接拒绝
top_k 过大限制在合理范围
数据库不存在给明确错误
skill 文件过大跳过并记录
编码错误跳过并记录
没有检索结果回退到目录分类

这些处理不复杂,但能让后续排障轻很多。

5. 第三层:SkillRouter 适合更大的规模
#

如果 skills 数量继续增长,只靠 embedding top-k 会遇到两个问题:

  • 相似但不该用的 skill 会被召回
  • 多领域任务需要更细的重排逻辑

这时可以考虑 retrieve-and-rerank,也就是先召回一批候选,再做更精细的排序。SkillRouter 这类方案就是这个方向。

flowchart LR
    query["用户任务"] --> encode["任务编码"]
    encode --> retrieve["召回候选
Top 50"] retrieve --> rerank["重排
Top 5"] rerank --> load["加载 1-3 个 skills"]

它的好处是路由更准,尤其适合上百、上千个 skills 的场景。代价是工程复杂度会上来:模型部署、延迟、评测集、回退策略都要补齐。

我的判断是:如果团队还没有稳定的目录分类和基础检索,不要急着上 reranker。先把前两层做扎实。

5.1 什么时候再上 reranker
#

满足下面几个条件,再考虑第三层:

  • skills 数量已经超过几百个
  • 向量 top-k 经常召回相似但错误的 skill
  • 团队有真实任务样本可以做评测
  • 能接受额外推理延迟和模型维护成本

否则 reranker 会变成一个难排查的新黑盒。

6. 效果怎么衡量
#

我不建议只看“减少了多少 tokens”。这个指标重要,但不够。真正要看四件事:

指标说明
Context 消耗每次任务实际加载多少 skill 内容
命中率该用的 skill 是否被找出来
误召回不相关 skill 是否频繁被加载
可解释性出错时能否判断为什么选了这个 skill

按当时的粗略统计,几种方式的体感差异是这样的:

方案加载规模Context 压力准确性维护成本
全量加载123 个很高高,但浪费
目录分层15-20 个
向量检索5-10 个中高
路由重排1-3 个很低

这里的数字不是实验室 benchmark,更像工程侧的容量估算。它能帮助判断方向,但不能替代真实任务评测。

6.1 建议的验收标准
#

第一版不用追求极致。可以先定这几个门槛:

指标建议门槛
Top-5 命中率80% 以上
Top-1 命中率60% 以上
平均加载 skills10 个以内
空结果率5% 以下
fallback 可用性目录分类能兜底

如果这些指标达不到,优先改 description 和目录,而不是换模型。

6.2 验证矩阵
#

落地时我会把验证拆成四类,避免只看单一的 token 数。

验证项方法通过标准
加载范围统计每类任务实际加载的 skills 数量普通任务加载 10 个以内
命中质量用 10-20 个真实任务样本跑 top-kTop-5 命中率达到 80% 以上
可解释性检查每个命中结果是否能说清触发原因不出现“分数高但原因不明”的常态
回退能力故意让语义检索空结果能回退到目录分类,不影响继续工作

7. 我会怎么落地
#

如果现在重新做一次,我会按这个顺序:

  1. 先盘点 skills

    删除重复、过期和几乎不用的 skill。很多 context 问题不是路由问题,而是库存问题。

  2. 建立目录边界

    至少拆出 corebackendfrontenddevopscontentdata。目录不是为了整洁,而是为了让加载范围可控。

  3. 给每个 skill 写好 description

    检索质量很大程度取决于 description。不要只写“用于某某技术”,要写清楚什么时候触发、解决什么问题、不要用于什么场景。

  4. 再做向量索引

    只索引 SKILL.md 的高价值段落,不要把所有参考资料一股脑塞进去。

  5. 再考虑 reranker

    当 skills 数量大到 top-k 经常误召回时,再加 SkillRouter 这类重排模型。

8. 最小可用脚本和验证
#

先做一个能跑起来的版本,比一开始追求“完美路由”更有价值。

python generate_embeddings.py \
  --skills-dir "$HOME/.codex/skills" \
  --db-path "./embeddings"

python skill_retriever.py \
  --query "Spring Boot 接口鉴权" \
  --category "backend" \
  --top-k 5

配套的输入校验别省:

if not query or not query.strip():
    raise ValueError("query cannot be empty")

if top_k < 1 or top_k > 50:
    raise ValueError("top_k must be between 1 and 50")

if not Path(db_path).exists():
    raise ValueError(f"database does not exist: {db_path}")

这类小检查能省掉很多排障时间。

跑完脚本后,用固定样本验证:

python skill_retriever.py \
  --query "把生产升级整理成 Blowfish blog" \
  --category "content" \
  --top-k 5

预期至少应该看到文档写作、博客发布、Blowfish 相关 skill。如果结果跑到无关的图片生成或前端 UI,说明 description 或 category 需要调整。

8.1 继续和停止条件
#

这类优化容易从“小修加载策略”扩散成“重写整个 skill 系统”。我会先把继续和停止条件写清楚。

阶段可以继续的条件应该停止的信号
目录分层目标目录下的 skill 数量明显下降,常用任务仍能找到对应 skill常用任务找不到关键 skill
向量索引固定样本 top-5 能命中预期 skilldescription 太泛,命中结果无法解释
路由重排已有真实样本集和可接受的延迟预算还没有稳定目录和基础检索

如果某个阶段失败,先回退到上一层可解释方案。目录分层是最小 fallback;它不够智能,但最容易排查。

9. 常见问题
#

问题:向量检索看起来分数很高,但结果不对

可能原因是 description 太泛,或者长文档里的高频词干扰了语义空间。先改 skill 描述,再考虑换 embedding 模型。

问题:目录分层后跨领域任务漏 skill

给跨领域任务保留 core + 当前领域的组合加载方式。例如写生产复盘时,至少加载 corecontentdevops

问题:skills 越整理越复杂

先删掉低价值 skill。路由系统解决不了库存污染,最多只是把污染藏得更深。

10. 这次复盘后的调整
#

这次优化后,我对 skills 的看法变了:skill 不是越多越好,关键是能不能在正确的时机被加载。

几个原则后来一直沿用:

  • 默认不全量加载
  • 先用目录缩小范围
  • description 要写触发条件,不只写功能介绍
  • 检索结果要能解释
  • 复杂路由必须有回退方案

最终目标不是把 context 压到最低,而是在“能找到正确 skill”和“不浪费上下文”之间取一个稳定平衡。

11. 遗留项
#

这篇只沉淀了方向和最小闭环,还有几个问题需要后续再做:

遗留项原因后续动作
固定评测集没有样本就很难判断路由质量从真实任务中抽 10-20 条样本
description 质量审计检索效果主要取决于描述是否准确先审高频 skill,再扩展到全量
路由延迟预算reranker 会增加响应时间有稳定样本后再评估是否值得引入

可以复用的原则很简单:先降低加载范围,再提升检索准确性;先保留可解释 fallback,再引入更复杂的路由。

12. 相关资源
#

OpenClaw 实战 - 这篇文章属于一个选集。
6: 本文

相关文章

OpenClaw AI Agent 架构解析:多引擎联动与记忆系统
·427 字·3 分钟
AI AI Agent 架构 OpenClaw
当 AI Agent 遇上运维自动化:我的实践踩坑之路
·135 字·1 分钟
AI AI Agent 自动化 运维 OpenClaw
2026 AI Agent 工程化全景:从框架选型到生产落地
·1007 字·5 分钟
AI 系统架构 AI Agent 架构 DevOps