Files
go-tiny-claw/README.md
aruo 3e91c7d0a1 v1.6 System Prompt 工程化 + 渐进式技能发现
重构 system prompt 为动态组装系统:PromptComposer 聚合核心身份、AGENTS.md
项目规范和 Skills 目录;新增 read_skill 工具实现技能按需加载;包重命名
internal/context -> internal/prompt 消除 stdlib 冲突。
2026-05-17 00:05:31 +08:00

203 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# go-tiny-claw
一个轻量级的 AI Agent 引擎,基于 Go 语言实现标准的 **ReAct(Reasoning + Acting)** 循环模式。
## 项目背景
go-tiny-claw 受 Claude / OpenAI 的 Agent 概念启发,旨在提供一个简洁、可扩展的微型 Agent 框架核心。它定义了 Agent 与大模型交互的标准契约,让开发者可以快速搭建自己的 AI 助手应用。
## 核心架构
```
┌─────────────────────────────────────────────┐
│ AgentEngine │
│ ┌──────────┐ ┌──────────┐ │
│ │LLMProvider│ │ Registry │ │
│ └─────┬────┘ └─────┬────┘ │
│ │ │ │
│ ▼ ▼ │
│ 大模型推理服务 本地工具执行 │
└─────────────────────────────────────────────┘
```
### 模块说明
| 包 | 说明 |
|---|---|
| `internal/engine` | **AgentEngine** — 核心驱动,实现 ReAct 主循环 |
| `internal/provider` | **LLMProvider** 接口 — 与大模型通信的统一契约 |
| `internal/schema` | 核心数据结构定义(Message、ToolCall、ToolResult) |
| `internal/tools` | **Registry** 接口 — 工具的注册与分发执行 |
### ReAct 循环流程
1. 系统初始化 Context,注入用户指令
2. 向大模型发起推理请求(含可用工具列表)
3. 模型返回文本回复和/或工具调用请求
4. 如果模型未请求工具调用 → 任务完成,退出循环
5. 执行模型请求的工具,获取 Observation
6. 将 Observation 追加到上下文,回到步骤 2
## 快速开始
```go
package main
import (
"context"
"go-tiny-claw/internal/engine"
"go-tiny-claw/internal/schema"
)
func main() {
provider := &mockProvider{}
registry := &mockRegistry{}
eng := engine.NewAgentEngine(provider, registry, "/path/to/workdir")
eng.Run(context.Background(), "你的指令")
}
```
## 扩展
- **替换 Provider**: 实现 `LLMProvider` 接口即可接入任意大模型(OpenAI、Claude、本地模型等)
- **注册工具**: 实现 `Registry` 接口即可挂载自定义工具集(bash、文件操作、代码搜索等)
- **自定义 Schema**: 基于 `schema` 包的数据结构可灵活扩展消息格式和工具定义
## 版本历史
### v1.5 — Edit 工具 + 四级容错替换 + 多工具并发执行
#### 变更
- **新增 EditFileTool** — 实现四级容错降级替换算法(L1 精确 → L2 换行符归一 → L3 Trim Space → L4 逐行去缩进滑动窗口),解决大模型代码修改时缩进丢失、换行符不一致等幻觉问题
- **工具集扩展** — 工具集从 3 个(read / write / bash)扩展到 4 个(+ edit)
- **多工具并发执行** — 引擎从串行改为并行:预分配结果切片 + `sync.WaitGroup` + 按索引无锁写入,模型一次请求多个工具时同时执行
#### 踩坑记录
| 问题 | 原因 | 解决 |
|---|---|---|
| 大模型生成的代码缩进不一致 | 模型推理时对源文件缩进感知不准,产生多一个空格或少一个 tab | 编辑工具内建多级模糊匹配 |
| 同一段代码在文件中出现多次 | 模型给的 old_text 上下文不够 | 算法检测多匹配后返回错误给模型自愈 |
| Windows 换行符 `\r\n` vs `\n` 不一致 | 模型通常输出 `\n`,Windows 文件可能是 `\r\n` | L2 换行符归一化:统一转 `\n` |
| Goroutine 闭包捕获 loop 变量 | Go 的 loop 变量复用同一地址 | 将 `i`/`toolCall` 作为参数传入 goroutine |
#### 经验教训
1. **Agent 工具要做"容错输入,严格输出"** — 接受模型可能不完美的输入(多级模糊匹配),但输出清晰的错误信息帮模型自我纠正
2. **工具语义要匹配模型的能力边界** — `edit_file`(给 old_text + new_text)比重写整个文件更适合 Agent,不要求模型完整认知整个文件
3. **并发安全可以零成本** — 预分配切片 + 按索引写入 + 主 goroutine 串行读取,比加锁方案更简洁高效
### v1.6 — System Prompt 工程化 + 渐进式技能发现
#### 变更
- **PromptComposer** — 新增 `internal/prompt` 包取代硬编码 system prompt,运行时动态组装:核心身份 → `AGENTS.md` 项目规范 → Skill 目录
- **AGENTS.md** — 项目根目录的 Markdown 文件自动被识别并注入 system prompt,让非代码层面的架构规范可被 Agent 感知
- **渐进式技能发现** — `SkillLoader` 扫描 `skills/<name>/SKILL.md`,system prompt 只注入技能目录(名称 + 一行描述),模型按需调用 `read_skill` 加载完整指令
- **ReadSkillTool** — 工具集扩展到 5 个,接收技能名称返回完整 Skill.md 正文
- **包重命名** — `internal/context` → `internal/prompt`,消除与标准库 `context` 包的命名冲突
#### 踩坑记录
| 问题 | 原因 | 解决 |
|---|---|---|
| 包名与标准库冲突 | `internal/context` 与 `context` 包同名,导入时被迫黑别名 `ctxpkg` | 重命名为 `prompt`,导入无歧义 |
| prompt 全量注入浪费上下文 | List() 只返回摘要,Build() 仍需知道技能的完整存在 | 摘要注入 system prompt,正文通过 read_skill 按需加载,典型场景节省 70%+ context |
#### 经验教训
1. **上下文管理是 Agent 的核心杠杆** — system prompt 从 30 行硬编码字符串演变为动态组装系统,每项内容(身份 / 项目规范 / 技能)的增删都不需要改引擎代码
2. **"先摘要后按需"适用于 Agent 的所有知识注入** — 无论是技能、文档还是 API 参考,把完整内容塞进 context 是最简单的做法,但渐进式加载才是可扩展的方案
### v1.4 — 工具集扩展与 Windows 编码攻坚
#### 变更
- **新增 BashTool** — 执行本地 bash 命令,支持 30s 超时、错误原样回传(模型自愈)、8KB 输出截断
- **新增 WriteFileTool** — 写文件到工作区,覆盖/新建均支持
- **Windows GBK 编码修复** — 命令输出从 GBK 自动转 UTF-8(`golang.org/x/text/encoding/simplifiedchinese`),不依赖 `chcp`
- **API 交互日志** — `OpenAIProvider.Generate` 新增请求/响应日志(阶段标记、消息数、工具数、ToolCall 明细)
- **调试输出优化** — `dumpMessages` 移除 80 字符截断,完整展示上下文内容
- **三段式任务演示** — `cmd/claw` 任务改为:查 Go 版本 → 写 helloworld.go → 编译运行
#### 踩坑记录
| 问题 | 原因 | 解决 |
|---|---|---|
| bash 输出中文乱码 | Windows 命令输出为 GBK 编码,Go 按 UTF-8 解析 | 引入 `golang.org/x/text`,检测编码后自动转换 |
| `chcp 65001` 导致终端刷屏 | `chcp` 在子进程中修改代码页可能影响终端渲染 | 放弃 `chcp` 方案,改用 Go 原生编码转换 |
| `utf8.Valid` 检查后仍乱码 | GBK 字节序列碰巧也合法于 UTF-8,跳过转换 | 去掉 `utf8.Valid` 判断,Windows 下一律转换 |
| `helloworld.go` 导致编译失败 | 模型生成的测试文件含 `main` 函数,与项目 main.go 冲突 | 运行后清理 `helloworld.*` 测试产物 |
#### 经验教训
1. **编码问题不要依赖外部命令** — `chcp` 属于"改环境让输出配合你",不可靠。Go 原生转码属于"你主动适应输出",稳定可控
2. **`utf8.Valid` 不能当编码检测器** — 它的语义是"是否合法 UTF-8",不是"是否是 GBK"。GBK 和 UTF-8 有交集,用合法性判断编码方向是伪命题
3. **Agent 的工具越多,越需要关注副作用** — 模型会写文件、执行命令,产生的文件(helloworld.go)可能反过来破坏项目结构。工具内部要做好隔离
### v1.3 — Registry 实现重构与第一把真实工具
#### 变更
- **Registry 完整实现** — 从纯接口升级为 `registryImpl`,基于 `map[string]BaseTool` 实现 O(1) 路由查找、动态注册、错误自愈
- **BaseTool 接口** — 定义工具的通用契约(`Name()` / `Definition()` / `Execute()`),所有具体工具统一实现
- **首个真实工具 ReadFileTool** — 支持读取工作区文件,含路径穿越防护和 8000 字节截断
- **调试可视化** — `AgentEngine` 和 `registryImpl` 实现 `String()` 方法,替代 16 进制内存地址
- **ToolCall 执行日志** — 每轮工具执行后输出一行状态日志(`📋 ToolCall xxx: ✅ / ❌, 结果: ...`)
- **cmd/claw 真实化** — 从 mockRegistry 切换到 `tools.NewRegistry()` + `ReadFileTool`
#### 踩坑记录
| 问题 | 原因 | 解决 |
|---|---|---|
| `fmt.Println(registry)` 输出 16 进制地址 | Go 默认打印指针/接口类型为内存地址,不展示内容 | 实现 `fmt.Stringer` 接口,自定义 `String()` 方法 |
| Registry 接口加 `Register` 后旧代码编译失败 | `main.go` 的 mockRegistry 没有实现新增的 `Register` 方法 | 为 mockRegistry 补充空实现 `Register(tool tools.BaseTool) {}` |
| 修改代码时需要同时兼容旧 mock 和新实现 | demo 代码和正式代码共用同一套接口 | 保留 mock 的兼容性(无操作 Register),新演示代码使用真实实现 |
#### 经验教训
1. **接口先于实现,但实现要跟上** — v1.0 就定义了 `Registry` 接口,但一直没有真实实现,导致 cmd 只能靠 mock 跑。抽象要尽早落地
2. **工具系统用 map 路由天然适合 Agent** — 大模型输出工具名 → 直接 map key 查找 → O(1) 路由,简单高效,也方便运行时动态挂载工具
3. **`String()` 是 Go 调试的性价比之王** — 三行代码换来看日志时不用猜内存地址,投入产出比极高
### v1.2 — 真实模型接入与 Thinking 死循环修复
#### 变更
- **接入真实大模型** — 新增 `OpenAIProvider` 和 `ClaudeProvider`,分别基于 OpenAI V3 SDK 和 Anthropic SDK 连接 Deepseek API,替换原有的 Mock Provider
- **Deepseek thinking 模式适配** — 修正 `reasoning_content` 字段丢失导致的 API 400 错误,在 `schema.Message` 中新增字段并在请求中回传
- **Thinking 死循环修复** — 过渡指令只在首轮插入,防止模型每轮重复调用工具
- **最大轮数保护** — 新增 `maxTurns = 10` 上限,防止意外死循环
- **调试输出** — 新增 `dumpMessages` 和 `dumpTools`,每轮打印上下文消息和工具列表
#### 踩坑记录
| 问题 | 原因 | 解决 |
|---|---|---|
| API 400: `reasoning_content must be passed back` | Deepseek thinking 模式返回了 `reasoning_content` 字段,回传请求时未携带 | 从响应 RawJSON 中提取该字段,用 `param.Override` 注入原始 JSON 回传 |
| 模型在 Phase 2 不调用工具 | Phase 1 的思考内容作为 Assistant 消息追加后,模型认为对话已结束 | 在思考后插入一条 User 角色过渡指令:"根据推理,使用工具完成任务" |
| 引擎死循环跑到 maxTurns | 过渡指令每轮都插入,模型每轮都被要求调用工具 | 过渡指令只在 `turnCount == 1` 时插入,后续轮次模型根据观察结果自行判断 |
#### 经验教训
1. **上下文即状态** — Agent 的所有行为都由上下文驱动。插入一条消息就能改变模型行为,不需要改代码逻辑
2. **非标准 API 字段需手动处理** — 大模型厂商的扩展字段(如 `reasoning_content`)不在 SDK 类型中,需要从 RawJSON 手动提取并用注入方式回传
3. **过渡指令的作用域很重要** — "使用工具"这种指令适合在首轮引导,重复出现会导致模型无法自行判断任务是否完成
### v1.1 — 慢思考模式 (Thinking Phase)
将 ReAct 循环从单阶段升级为双阶段架构:
- **Phase 1 (Thinking)** — 剥夺工具访问权,强制模型先进行纯文本推理规划
- **Phase 2 (Action)** — 恢复工具挂载,模型顺着推理结果执行精准的工具调用
- 新增 `EnableThinking` 开关,兼容旧模式
### v1.0 — ReAct 循环基础
实现标准的单阶段 ReAct 循环:思考 → 调工具 → 观察 → 继续,直到任务完成。
## License
MIT