SmallCode 上下文预算管理深度分析
Codex 核验(2026-10-09):本文件是项目源码研究与 Hermes 移植提案,未完成集成。当前 Ollama 支持
think,具体布尔/等级控制依模型;2000thinking budget 不能跨模型统一生效,响应后的 32000 字符截断也不能限制已生成或已计费推理。Hermes 主配置是config.yaml。工具名合法仍须校验参数 schema、权限与作用域;正则修 JSON 可能误改字符串。固定提示有利缓存,但命中取决于服务和前缀;70% 窗口预算需包含系统提示、工具及回复预留,schema 减少比例不能等同总费用降幅。
基于 SmallCode v1.3.0 (https://github.com/Doorman11991/smallcode)
研究日期:2026-05-28
分析目标:提取核心上下文预算管理策略,迁移为 Hermes Agent 的small-model-compensationskill
一、概述
SmallCode 是一个专为小型 LLM(8B-35B 参数)设计的终端原生编码代理。与假设前沿模型(128k+ 上下文、完美 JSON 输出)的工具不同,SmallCode 从相反的假设出发:模型可能只有 8-32k 上下文窗口,输出的工具调用格式不规范,并且会在三步之后忘记自己在做什么。
核心成就:使用仅 4B 活跃参数的 Gemma 4 E4B 模型,在单文件任务上达到 87% 成功率,超越使用 3-4 倍大模型的竞品。
1.1 设计哲学
SmallCode 的每个架构决策都源于一个核心约束:从限制中提取有用工作。它不是更聪明的 Cursor,而是从消费级硬件上运行的模型中榨取效能。
1.2 与 Hermes Agent 的对比
| 维度 | SmallCode 方案 | Hermes Agent 当前状态 |
|---|---|---|
| 目标模型 | 8B-35B 本地模型 | 通用(强模型优先) |
| 上下文管理 | 预算控制、压缩、逐出 | 无显式管理 |
| 工具调用 | 宽容多格式解析器 | 假设标准格式 |
| 编辑方式 | Patch 优先 | 支持但非首选 |
| 规划 | TODO 文件分解 | 无显式规划 |
二、上下文预算引擎(Context Budget Engine)
这是 SmallCode 最核心的创新,直接解决小模型上下文窗口有限的痛点。
2.1 三层预算控制
┌─────────────────────────────────────────────────┐
│ Layer 1: 预调用检查 (assertWithinBudget) │
│ ・每分钟最多 30 轮 (run_turn) │
│ ・每小时最多 500K tokens │
│ ・超出则抛 BUDGET_EXCEEDED │
├─────────────────────────────────────────────────┤
│ Layer 2: 调用中预算 (per trace) │
│ ・LLM_BUDGET_TOKENS_PER_TRACE (token 上限) │
│ ・LLM_BUDGET_USD_PER_TRACE (USD 上限) │
│ ・成本类: tiny/small/medium/large │
├─────────────────────────────────────────────────┤
│ Layer 3: 上下文窗口管理 (运行时) │
│ ・max_budget_pct = 70% 窗口 │
│ ・每 3 次工具调用检查一次 │
│ ・超出 60% 窗口 → 触发压缩 │
└─────────────────────────────────────────────────┘
2.2 轮中逐出(Mid-Turn Eviction)
当 estimated_history_tokens > detected_window * 0.6:
第一步:截断旧的 tool_call 参数
- 找到最后一个 assistant 消息之前的旧消息
- 将大于 200 字符的 JSON 参数替换为最小化版本
- 字符串值截断到 80 字符后加
… - 如果已经是无效 JSON,重置为
{}
第二步:逐出旧的 tool 结果
- 从最旧的消息开始,仅当 owner 在前半部分历史中
- 将 tool 结果替换为
[evicted: N tokens]标记 - 孤立结果(owner 已经被移除)直接 splice 删除
- 释放的 token 计入
midEst递减
第三步:验证
- 如果仍超出
maxBudget * 0.7,继续逐出 - 记录逐出次数到
tokenMonitor.recordEviction()
2.3 工具结果上限(Tool Result Capping)
SMALLCODE_MAX_TOOL_RESULT_CHARS = 8000 (默认)
对于读操作(read_file, find_and_read 等),使用上下文感知守卫:
// 检查两个触发条件
condition1 = context_usage > budget_pct // 实时上下文已超预算
condition2 = file_tokens * 2 > context_window // 单文件占窗口 50%+
if (condition1 || condition2) {
// 激进裁剪:仅保留前 30 行 + 提示
// "Context budget is tight. Use search/find_files..."
} else {
// 标准裁剪:保留头 70% + 尾 20% + 中间截断标记
}2.4 大文件自动摘要
- 文件 > 200 行 → 自动 LLM 摘要
- 提取函数签名和关键逻辑
- 按内容哈希缓存(1h TTL)
- 目标 token 数:600
- 摘要工具结果替代原始文件内容
2.5 系统提示缓存分离(Cache-Split)
这是针对 llama.cpp KV-cache 的优化:
静态部分(每次相同,可缓存):
- 身份、OS、工作目录
- 核心规则(patch 优先、复合工具、保持简洁)
- 大文件规则(60 行/8KB 限制)
动态部分(作为单独的 [CONTEXT] 用户消息):
- 相关记忆(得分 ≥ 2,最多 800 tokens)
- Skill 上下文(最多 1000 tokens)
- 知识笔记(最多 1500 tokens)
效果:llama.cpp 每次调用不再使 KV-cache 失效,减少重复计算。
三、宽容工具调用解析器(Forgiving Tool Call Parser)
小模型输出不规则,SmallCode 设计了多层恢复机制。
3.1 嵌入式工具调用检测
当模型将工具调用作为文本嵌入 content 而非 tool_calls 字段:
| 格式 | 正则模式 | 来源 |
|---|---|---|
<tool_call>{...}</tool_call> | Hermes/Qwen2.5 原生格式 | |
```json\n{...}\n``` | fenced JSON 块 | Qwen-coder 等 |
```tool_call\n{...}\n``` | fenced tool_call | Llama3 微调版 |
| 纯 JSON 对象 | { "name": "...", "arguments": {...} } | 最后的尝试 |
处理流程:
message.tool_calls exists? → 直接使用
↓ 否
匹配 <tool_call> 标签 → 提取 JSON
↓ 无匹配
匹配 ```json/```tool_call 代码块 → 提取 JSON
↓ 无匹配
匹配纯 JSON 对象(内容基本都是 JSON)→ 提取
↓ 无匹配
保持原样(作为文本回复处理)
3.2 JSON 修复(Repair)
当 JSON.parse 失败时:
- LLM 自修复:将
original_call + error + schema发回模型 - 正则后备:针对 write_file,用正则从损坏 JSON 中提取 path/content
- 空后备:全部失败则返回
{}并记录错误
3.3 格式标准化
将各种工具调用格式统一为标准形式:
{ name, arguments } → { name, arguments }
{ function: { name, arguments } } → { name, arguments }
{ tool, args } → { name, arguments }
只在工具名在已知列表中时接受。
3.4 JSON 清理
自动去除尾随逗号(Qwen 常见问题):
cleaned = trimmed.replace(/,\s*([}\]])/g, '$1');支持多行 JSON 对象(换行符分隔)逐个解析。
四、Patch 优先编辑模式
4.1 为什么 Patch 优先
小模型的全文件重写有三个主要问题:
- 截断:超出 token 限制后直接截断
- 幻觉导入:添加不存在的 import
- 缩进漂移:逐行生成时缩进逐渐变化
Patch(搜索-替换)方式:
- 只修改需要改动的 10-20 行
- old_str 必须精确匹配一个位置 → 模型必须准确
- 上下文成本远低于全文件重写
4.2 语义合并回退
当 patch 失败(old_str 不匹配,因之前编辑导致文件变动):
- 将当前文件内容和预期的改动发回模型
- 模型返回整个文件的修正版本
- 这是最后手段,非首选
4.3 读前写守卫(Read-Before-Write Guard)
首次对未读文件执行 write_file → 拒绝,提示 "先读取文件"
第二次尝试 → 允许(合法覆盖)
防止小模型在未理解文件内容的情况下直接覆盖。
4.4 大小限制
write_file: max 60 行 / 8KB → 骨架文件 + 多次 patch 填充
append_file: max 60 行 / 8KB/次 → 增量构建大文件
patch: 无内容限制 → 精确改动
llama.cpp 的 JSON 解析器在 ~13KB 的 tool_call 参数上会崩溃,这是硬限制的根源。
五、两阶段工具路由(Two-Stage Tool Routing)
5.1 机制
对 ≤ 16k 上下文窗口的模型:
阶段 1:模型选择类别(仅 ~200 tokens):
read: 读取文件、查找文件
write: 创建文件、patch
search: 正则搜索、图谱搜索
run: shell 命令
plan: 记忆、BoneScript
阶段 2:系统仅注入该类别的工具 schema(~400 tokens)
直接模式:对 > 16k 上下文的模型,一次性发送所有工具。
5.2 节省计算
全部工具 schemas: ~2000 tokens
选择器工具: ~100 tokens
类别工具: ~400 tokens
节省: 约 75%
5.3 确认守卫(Affirmation Guard)
当模型在任务中间回复 “yes”/“ok” 时,保持之前的工具类别而不是重新分类为 respond。否则分类器会将确认作为无工具类别处理,导致模型失去编写工具。
5.4 确定性路由
正则分类器基于 8 个类别的加权评分:
- 正向信号(提升分数)+ 反向反信号(降低分数)
- “Explain” 降低 write 分数
- “All uses of” 提升 search 分数
- 平局时优先级:write > run > code-intelligence > search > plan > read > web > respond
六、改善循环(Improvement Loop)
6.1 自校验流程
每次 write/patch 后:
- 语义评审(LLM-based):检查编辑是否有逻辑问题
- 语法检查:
py_compile,node --check,tsc --noEmit,go build - 重试循环:错误反馈给模型(最多 2 次)
- 分解策略:重试耗尽后切换策略
- 升级:本地模型失败后可选升级到云端模型
6.2 分解策略(Decompose)
当 2 次重试都失败时:
- 用 LLM 分析失败原因,选择新策略
- 策略类型:重写整个文件、重写特定部分、完全从零开始
- 如果分解也失败 2 次,触发升级
6.3 信任衰减(Trust Decay)
追踪每个工具的连续失败次数:
- 3+ 次失败且置信度 < 0.35 → 从 schema 列表中移除
- 提供替代工具建议
- 跨 session 持久化
6.4 快照与自动回滚
每次 agent 轮次前创建快照:
- 记录每个编辑前的文件内容
- 验证全面失败后自动回滚
- 存储在
.smallcode/snapshots/
七、早期停止检测(Early-Stop Detection)
7.1 重复循环检测
在流式输出期间检查:
- 窗口大小:50/80/120 字符
- 阈值:3+ 次重复
- 动作:注入修正提示
7.2 Patch 螺旋检测
当模型在同一个文件上卡住:
- 4+ 次 patch 失败 → 强制全文件重写
- 6+ 次总尝试 → 同样处理
- 空操作 patch(old_str === new_str)也计为失败
7.3 只读循环检测
连续只读工具调用:
- 5 次无输出 → 温和提示
- 8 次无输出 → 强制停止 + 注入 “你已经有足够上下文,停止阅读,开始编写”
7.4 问候回归检测
如果模型在任务中间输出问候语:
- 检测模式:“how can i help”, “hello!”, “what can i do for you”
- 注入:“你输出了问候语而非完成任务,继续之前的工作”
八、思考预算控制(Thinking Budget Control)
8.1 背景
推理模型(Qwen3, DeepSeek R1, Claude with thinking)在给出答案前会输出大量 “thinking” 令牌。如果不加控制,一个简单的重命名操作可能消耗 8000 tokens 的思考。
8.2 预算注入
DEFAULT_BUDGET_TOKENS = 2000 // SMALLCODE_THINKING_BUDGET
HARD_CAP_CHARS = 32000 // SMALLCODE_THINKING_HARD_CAP按提供商差异化:
| 提供商 | 字段 |
|---|---|
| Anthropic | thinking: { type: "enabled", budget_tokens: N } |
| OpenAI (o1/o3/o4) | reasoning_effort: "low"/"medium"/"high" |
| llama.cpp/LM Studio | chat_template_kwargs: { enable_thinking, thinking_budget } |
| Ollama | 当前支持 think;按模型能力映射,不能统一套用数字预算 |
8.3 思考提取与截断
- 从回复中提取
<think>...</think>块 extractThinking(content)→{ thinking, answer }- answer 存入对话历史,thinking 仅用于显示
- 超硬上限截断:保留前 60% + 后 30%,中间替换为
[...thinking truncated: N chars omitted...]
8.4 重试时禁用思考
在修复循环(attempt > 1)时自动禁用思考——模型已经在第一次超常思考,快速确定性修复更好。
九、TODO 驱动的规划系统
9.1 计划提取
对于多步骤任务:
- 注入一次性指令,要求模型在工具调用前输出编号计划
- LLM 提取器将自然语言中的计划结构化为步骤列表
- 正则表达式作为后备
9.2 计划锚重新注入
每轮将计划注入系统提示:
ACTIVE PLAN (step 3 of 5):
✓ 1. Read the existing auth module
✓ 2. Identify the JWT validation function
→ 3. Add the refresh token handler
4. Update the route middleware
5. Run tests
9.3 依赖图
新功能:检查计划步骤是否触及同一文件 → 拓扑排序 → 并行批次
十、可迁移到 Hermes Agent 的技术
10.1 高优先级(P0)
| 技术 | SmallCode 实现 | Hermes Agent 集成方式 |
|---|---|---|
| 上下文预算控制 | max_budget_pct=70%, detected_window | 提案:在 Hermes config.yaml 中设计 small_model 配置;需另行实现并验证 |
| 工具结果上限 | 8000 字符 + 上下文感知 30 行守卫 | 覆盖 _executeToolModule 的 result 处理 |
| Patch 优先编辑 | 搜索-替换 + 语义合并回退 | 强化 patch 工具,弱化 write_file |
| 宽容 JSON 解析 | 5 种格式 + LLM 修复 + 正则后备 | 在 tool 执行前加一层 tool_call_extractor |
10.2 中优先级(P1)
| 技术 | 说明 |
|---|---|
| 两阶段工具路由 | 对小上下文模型只发类别选择器,第二批发特定类别工具 |
| 改善循环 | 写文件后自动验证 + 重试 + 分解 + 升级 |
| 早期停止检测 | 重复/螺旋/只读循环/问候回归 四类检测 |
| 思考预算 | 为推理模型设置思考 token 上限 |
10.3 低优先级(P2-P3)
| 技术 | 说明 |
|---|---|
| 系统提示缓存分离 | 静态+动态分离,避免 KV-cache 失效 |
| TODO 规划 | 计划提取+锚注入+依赖图 |
| 信任衰减 | 工具级别置信度追踪 |
| 工具评分 | 贝叶斯学习 |
10.4 不直接迁移的部分
| 特性 | 原因 |
|---|---|
| BoneScript | 特定于 Node.js 后端的编译系统 |
| MarrowScript | SmallCode 专属的编译层 |
| MCP 代码图谱 | 依赖 budget-aware-mcp 扩展 |
| 模型升级 | Hermes Agent 自有模型管理 |
| 快照/回滚 | SmallCode 特有的 session 系统 |
十一、架构图视角
┌─────────────────────────────────────────────────────────────────────┐
│ SmallCode 上下文预算管理全景 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 用户输入 │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ 1. 输入预处理 │ ← 正则分类器(0 token 成本) │
│ │ ・模糊检测 │ │
│ │ ・@file 展开 │ │
│ │ ・git diff 注入 │ │
│ └─────────┬───────────┘ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ 2. 工具路由 │ ← 两阶段模式节省 ~75% 工具 schema tokens│
│ │ ・确定性类别选择 │ │
│ │ ・两阶段(≤16k) │ │
│ └─────────┬───────────┘ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ 3. 系统提示构建 │ ← 缓存分离(静态+动态) │
│ │ ・记忆/知识注入 │ │
│ │ ・计划/测试注入 │ │
│ │ ・所有部分都有上限 │ │
│ └─────────┬───────────┘ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ 4. API 调用 │ ← 思考预算注入 │
│ │ ・思考提取+截断 │ │
│ └─────────┬───────────┘ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ 5. 工具调用处理 │ ← 宽容解析(5 种格式+修复) │
│ │ ・调用→执行 │ │
│ │ ・结果→上限+守卫 │ │
│ │ ・每 3 轮检查上下文│ │
│ └─────────┬───────────┘ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ 6. 改善循环 │ ← 验证+重试+分解+升级 │
│ │ ・语义评审+语法 │ │
│ └─────────┬───────────┘ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ 7. 早期停止 │ ← 4 类退化检测 │
│ │ ・重复/螺旋/只读 │ │
│ │ ・问候回归 │ │
│ └─────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
十二、结论与建议
12.1 关键发现
- 上下文预算是第一优先级:SmallCode 在每 3 次工具调用后检查上下文使用情况,这比后端简单截断消息历史更精确
- 宽容解析器是多步任务生命线:小模型经常输出格式不正确的工具调用,没有修复机制的话大多数多步任务会失败
- Patch 优先比直觉更有效:搜索-替换比全文件重写更省 token 且更可靠,尤其是对经常截断的小模型
- 系统提示缓存分离很巧妙:直接解决本地推理引擎(llama.cpp)的 KV-cache 失效问题
- Improvement Loop 用工程弥补模型能力:模型不需要一次做对,系统自动验证和重试
12.2 迁移建议
small-model-compensation skill 的 MVP(最小可行) 应包含:
- 上下文预算控制 → 防止 8k 窗口溢出
- 工具结果上限 → 控制每次调用的 token 消耗
- 宽容 JSON 解析 → 让 Hermes 能处理 Qwen/Llama 等模型的非标准输出
- Patch 模式强调 → 减少全文件写入操作
扩展功能(后续迭代): 5. 两阶段工具路由 → 进一步减少 token 消耗 6. 改善循环 → 自动验证+重试 7. 早期停止 → 避免 token 浪费在退化行为上
12.3 风险与注意事项
- Latency 增加:改善循环、LLM 修复调用会产生额外延迟
- 过度工程:对于已有大模型(GPT-4, Claude 4)用户,这些补偿无用且增加复杂度
- 配置复杂度:过多的环境变量和配置项可能需要简化
- 模型适配性:不同小模型有不同的弱点,需要可配置的 profile 系统
本分析基于 SmallCode v1.3.0 (2026-05-28),MIT 许可证。 代码库:https://github.com/Doorman11991/smallcode Skill 草案位置:~/.hermes/skills/small-model-compensation/SKILL.md