mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-06 01:07:35 +00:00
feat(skillmgr): skill_info 支持按章节读取 —— 长 SKILL 不再一次吞一万 token
## 问题
skill_info 此前**整份返回** SKILL.md。生产最坏一例:
commondev-discipline v2.3.0 → 983 行 / 53810 字节 / ≈10600 token
模型为查「怎么开 PR」(§14,21 行)必须把 hmap 插件开发纪律(320 行)
和红牌一起读进上下文。一次误读一万 token,还挤掉了真正要用的历史。
拆 skill(一个插件声明多个)能解决"跨文档绑定",但解决不了
"单文档内部按需读" —— 本提交补的就是后者。
## 改法
skill_info(name, section=...) 只返回匹配的章节。不传 section 时:
短文档(<=200 行) 直接返回全文(加目录只是噪音)
长文档(>200 行) 返回「目录 + 开头 30 行」,模型据此决定读哪节
目录每行都标 `(N 行)` —— 行数不是装饰,它是模型在"一次吞一万 token"
和"先读目录"之间做选择的唯一依据。
## 三个实现要点
① **切分必须感知围栏代码块**
SKILL.md 里有大量 shell 注释:
```bash
# 期望 200;401/403 = 凭据无效
curl -s ...
```
硬按 `#` 切会把一条 curl 示例拆成两半 —— 模型拿到的是无法执行的
碎片,比不给还糟。parseSections 跟踪 ``` / ~~~ 围栏,块内不切。
② **返回行号区间,不复制字符串**
章节内容的唯一事实来源仍是文件本身,目录与正文用同一套边界计算,
不会漂移成两份。
③ **点名顶层章节给整棵子树**
问 `## Setup` 时要的是 §11–§16 全部,不是只有标题那一行。
边界 = 下一个**同级或更浅**的标题。
## 判据(均经变异验证)
TestSectionsRebuildEqualsOriginal ★ 主判据
所有区间拼接必须逐字节等于原文
一次同时抓漏行 / 重行 / 错位三类缺陷
变异:end 改 heads[i+1].line-1 ⇒ "原文 16 行,拼回 13 行"(实测红)
TestSectionsShellCommentIsNotHeading
变异:删掉 fence 判断 ⇒ "代码块内的 shell 注释被当成标题:
\"期望 200;401/403 = 凭据无效\"(行 4)"(实测红)
TestResolveSectionReturnsWholeSubtree
变异:`<=` 改 `<` ⇒ "Setup 越界吃进了 Usage 的内容"(实测红)
TestResolveSectionToleratesHashPrefix
模型常把 `## Setup: git 操作` 整行传进来,必须容错
TestRenderTOCListsLevels
目录只列到 maxLevel,且必须带行数
## 判据本身最容易写废的地方(已避开)
① 用人造短文档测切分 —— 那种文档没有 shell 注释、没有嵌套层级、
没有围栏代码块,测过了不代表能切真实文件。
主判据**用生产那份 983 行的真实文件**。
② 只断言"切出 N 个章节" —— 数量对不代表边界对。
## 真实数据验证
文档 983 行 → 切出 46 个章节(## 顶层 7 个)
区间拼接 == 原文(逐字节)
section="审批" → 21 行(§16)
section="Setup" → 123 行(含 §11–§16 全部子节)
section="hmap" → 320 行
## 验证
go build ./... PASS
go vet ./... PASS
gofmt -l 干净
go test ./... -count=1 63 包全过,0 FAIL
干净检出(worktree) build + skillmgr 全绿
This commit is contained in:
@ -60,6 +60,12 @@ func (p *Plugin) registerInfo() {
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"name": map[string]interface{}{"type": "string", "description": "技能名称"},
|
||||
"section": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "可选。只返回某个章节。不传则返回「目录 + 全文概要」," +
|
||||
"长文档应先用目录定位再按需读章节,避免一次吞掉上万 token。" +
|
||||
"可用章节标题的任一段(不区分大小写,支持子串匹配)。",
|
||||
},
|
||||
},
|
||||
"required": []string{"name"},
|
||||
},
|
||||
@ -76,13 +82,45 @@ func (p *Plugin) registerInfo() {
|
||||
var b strings.Builder
|
||||
fmt.Fprintf(&b, "名称: %s\n版本: %s\n状态: %s\n路径: %s\n描述: %s\n",
|
||||
e.sk.Name(), e.sk.Version(), state, e.path, e.sk.Description())
|
||||
|
||||
raw := e.sk.RawContent()
|
||||
sections := parseSections(raw)
|
||||
|
||||
// 带 section:只返回匹配的章节(这是长文档按需读的入口)。
|
||||
if want, _ := args["section"].(string); strings.TrimSpace(want) != "" {
|
||||
start, end, idx, ok := resolveSection(raw, sections, want)
|
||||
if !ok {
|
||||
var b2 strings.Builder
|
||||
b2.WriteString("没有匹配的章节。可用的章节:\n")
|
||||
b2.WriteString(renderTOC(sections, 3))
|
||||
return nil, fmt.Errorf("skill %q 没有匹配 %q 的章节\n%s",
|
||||
name, want, b2.String())
|
||||
}
|
||||
lines := strings.Split(raw, "\n")
|
||||
body := strings.Join(lines[start:end], "\n")
|
||||
fmt.Fprintf(&b, "\n--- %s(第 %d-%d 行)---\n%s\n", sections[idx].Title, start+1, end, body)
|
||||
return b.String(), nil
|
||||
}
|
||||
|
||||
if tools := e.sk.Tools(); len(tools) > 0 {
|
||||
b.WriteString("文档中声明的操作步骤:\n")
|
||||
for _, td := range tools {
|
||||
fmt.Fprintf(&b, " - %s: %s\n", td.Name, td.Description)
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(&b, "\n--- SKILL.md ---\n%s", e.sk.RawContent())
|
||||
|
||||
// 短文档(<= shortSkillLines)直接给全文:加一层目录只是噪音。
|
||||
lines := strings.Split(raw, "\n")
|
||||
if len(lines) <= shortSkillLines {
|
||||
fmt.Fprintf(&b, "\n--- SKILL.md ---\n%s", raw)
|
||||
return b.String(), nil
|
||||
}
|
||||
|
||||
// 长文档:先给目录 + 前若干行,模型据此决定读哪一节。
|
||||
fmt.Fprintf(&b, "\n--- 目录(本文 %d 行,请用 section 参数按需读取)---\n", len(lines))
|
||||
b.WriteString(renderTOC(sections, 3))
|
||||
fmt.Fprintf(&b, "\n--- 开头 %d 行 ---\n%s\n", tocHeadLines,
|
||||
strings.Join(lines[:minInt(tocHeadLines, len(lines))], "\n"))
|
||||
return b.String(), nil
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user