v1.6 System Prompt 工程化 + 渐进式技能发现
重构 system prompt 为动态组装系统:PromptComposer 聚合核心身份、AGENTS.md 项目规范和 Skills 目录;新增 read_skill 工具实现技能按需加载;包重命名 internal/context -> internal/prompt 消除 stdlib 冲突。
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"log"
|
||||
"sync"
|
||||
|
||||
"go-tiny-claw/internal/prompt"
|
||||
"go-tiny-claw/internal/provider"
|
||||
"go-tiny-claw/internal/schema"
|
||||
"go-tiny-claw/internal/tools"
|
||||
@@ -19,6 +20,7 @@ type AgentEngine struct {
|
||||
// WorkDir (工作区): 借鉴 OpenClaw 的理念,Agent 必须有一个明确的物理边界
|
||||
WorkDir string
|
||||
EnableThinking bool // 【新增】慢思考模式开关
|
||||
composer *prompt.PromptComposer
|
||||
}
|
||||
|
||||
func NewAgentEngine(p provider.LLMProvider, r tools.Registry, workDir string, enableThinking bool) *AgentEngine {
|
||||
@@ -27,6 +29,7 @@ func NewAgentEngine(p provider.LLMProvider, r tools.Registry, workDir string, en
|
||||
registry: r,
|
||||
WorkDir: workDir,
|
||||
EnableThinking: enableThinking,
|
||||
composer: prompt.NewPromptComposer(workDir),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,15 +63,11 @@ func (e *AgentEngine) Run(ctx context.Context, userPrompt string) error {
|
||||
log.Printf("[Engine] 引擎启动,锁定工作区: %s\n", e.WorkDir)
|
||||
log.Printf("[Engine] 慢思考模式 (Thinking Phase): %v\n", e.EnableThinking)
|
||||
|
||||
systemMsg := e.composer.Build()
|
||||
|
||||
contextHistory := []schema.Message{
|
||||
{
|
||||
Role: schema.RoleSystem,
|
||||
Content: "You are go-tiny-claw, an expert coding assistant. You have full access to tools in the workspace.",
|
||||
},
|
||||
{
|
||||
Role: schema.RoleUser,
|
||||
Content: userPrompt,
|
||||
},
|
||||
systemMsg, // 注入动态组装的内核、AGENTS.md 与 Skills
|
||||
{Role: schema.RoleUser, Content: userPrompt},
|
||||
}
|
||||
|
||||
turnCount := 0
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package prompt
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"go-tiny-claw/internal/schema"
|
||||
)
|
||||
|
||||
// PromptComposer 负责根据工作区环境动态生成 System Prompt
|
||||
type PromptComposer struct {
|
||||
workDir string
|
||||
skillLoader *SkillLoader
|
||||
}
|
||||
|
||||
func NewPromptComposer(workDir string) *PromptComposer {
|
||||
return &PromptComposer{
|
||||
workDir: workDir,
|
||||
skillLoader: NewSkillLoader(workDir),
|
||||
}
|
||||
}
|
||||
|
||||
// Build 组装并返回一条完整的 RoleSystem 消息
|
||||
func (c *PromptComposer) Build() schema.Message {
|
||||
var promptBuilder strings.Builder
|
||||
|
||||
// 1. 极简内核 (Minimal Core)
|
||||
promptBuilder.WriteString(`# 核心身份
|
||||
你名叫 go-tiny-claw,一个由驾驭工程驱动的骨灰级研发助手。
|
||||
你具备极简主义哲学,拒绝废话。你能通过系统提供的内置工具,创建、读取、修改和执行工作区中的代码。
|
||||
|
||||
# 核心纪律 (CRITICAL)
|
||||
1. 如需检查文件是否存在,请使用 bash 的 ls 或 test -f,而不是对目录使用 read_file。
|
||||
2. 创建新文件时,务必使用 write_file,并同时提供 path 和 content 参数。
|
||||
3. 编辑文件前务必先读取现有文件,以理解上下文。
|
||||
4. 无论何时你需要写代码或创建文件,都要直接使用 write_file 工具。
|
||||
5. 遇到工具执行报错时,仔细阅读 stderr,尝试自己修正命令并重试。
|
||||
6. 始终用中文回复,以便传达你的进展和想法。
|
||||
`)
|
||||
|
||||
// 2. 外部化状态:加载项目专属规范 (AGENTS.md)
|
||||
agentsMDPath := filepath.Join(c.workDir, "AGENTS.md")
|
||||
content, err := os.ReadFile(agentsMDPath)
|
||||
if err == nil {
|
||||
promptBuilder.WriteString("\n# 项目专属指南 (来自 AGENTS.md)\n")
|
||||
promptBuilder.WriteString("以下是当前工作区特有的架构规范与注意事项,你的行为必须绝对符合以下要求:\n")
|
||||
promptBuilder.WriteString("```markdown\n")
|
||||
promptBuilder.WriteString(string(content))
|
||||
promptBuilder.WriteString("\n```\n")
|
||||
}
|
||||
|
||||
// 3. 渐进式技能发现:仅注入目录(名称 + 触发条件),正文按需加载
|
||||
skills := c.skillLoader.List()
|
||||
if len(skills) > 0 {
|
||||
promptBuilder.WriteString("\n# 可用专业技能\n")
|
||||
promptBuilder.WriteString("你在以下场景可以调用 read_skill 加载完整的技能指令:\n\n")
|
||||
for _, s := range skills {
|
||||
promptBuilder.WriteString("- **" + s.Name + "**:" + s.Description + "\n")
|
||||
}
|
||||
promptBuilder.WriteString("\n当任务匹配上述技能的描述时,请先调用 read_skill 加载完整的执行指南,再严格按照指南操作。\n")
|
||||
}
|
||||
|
||||
return schema.Message{
|
||||
Role: schema.RoleSystem,
|
||||
Content: promptBuilder.String(),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package prompt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Skill 定义了从 SKILL.md 中解析出的标准化技能结构
|
||||
type Skill struct {
|
||||
Name string
|
||||
Description string
|
||||
Body string
|
||||
}
|
||||
|
||||
// SkillSummary 是技能的轻量目录项(仅元信息,不含 Body)
|
||||
type SkillSummary struct {
|
||||
Name string
|
||||
Description string
|
||||
}
|
||||
|
||||
// SkillLoader 负责从本地文件系统中加载技能模板
|
||||
type SkillLoader struct {
|
||||
workDir string
|
||||
}
|
||||
|
||||
func NewSkillLoader(workDir string) *SkillLoader {
|
||||
return &SkillLoader{workDir: workDir}
|
||||
}
|
||||
|
||||
// List 扫描 skills 目录,返回所有技能的轻量目录(仅名称和描述)
|
||||
func (s *SkillLoader) List() []SkillSummary {
|
||||
skillBaseDir := filepath.Join(s.workDir, "skills")
|
||||
if _, err := os.Stat(skillBaseDir); os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
|
||||
var summaries []SkillSummary
|
||||
|
||||
filepath.WalkDir(skillBaseDir, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if d.IsDir() || d.Name() != "SKILL.md" {
|
||||
return nil
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
skill := parseSkillMD(string(content))
|
||||
summaries = append(summaries, SkillSummary{
|
||||
Name: skill.Name,
|
||||
Description: skill.Description,
|
||||
})
|
||||
return nil
|
||||
})
|
||||
|
||||
return summaries
|
||||
}
|
||||
|
||||
// Load 按技能名称加载完整的 Skill(含 Body)
|
||||
func (s *SkillLoader) Load(name string) (*Skill, error) {
|
||||
skillBaseDir := filepath.Join(s.workDir, "skills")
|
||||
if _, err := os.Stat(skillBaseDir); os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("skills 目录不存在")
|
||||
}
|
||||
|
||||
var found *Skill
|
||||
|
||||
filepath.WalkDir(skillBaseDir, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil || found != nil {
|
||||
return nil
|
||||
}
|
||||
if d.IsDir() || d.Name() != "SKILL.md" {
|
||||
return nil
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
skill := parseSkillMD(string(content))
|
||||
if skill.Name == name {
|
||||
found = &skill
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if found == nil {
|
||||
return nil, fmt.Errorf("未找到技能: %s", name)
|
||||
}
|
||||
return found, nil
|
||||
}
|
||||
|
||||
// parseSkillMD 解析带有 YAML Frontmatter 的 Markdown 内容
|
||||
func parseSkillMD(content string) Skill {
|
||||
skill := Skill{
|
||||
Name: "Unknown Skill",
|
||||
Description: "No description provided.",
|
||||
Body: content,
|
||||
}
|
||||
|
||||
if strings.HasPrefix(content, "---\n") || strings.HasPrefix(content, "---\r\n") {
|
||||
parts := strings.SplitN(content, "---", 3)
|
||||
if len(parts) == 3 {
|
||||
frontmatter := parts[1]
|
||||
skill.Body = strings.TrimSpace(parts[2])
|
||||
|
||||
lines := strings.Split(frontmatter, "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "name:") {
|
||||
skill.Name = strings.TrimSpace(strings.TrimPrefix(line, "name:"))
|
||||
} else if strings.HasPrefix(line, "description:") {
|
||||
skill.Description = strings.TrimSpace(strings.TrimPrefix(line, "description:"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return skill
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"go-tiny-claw/internal/prompt"
|
||||
"go-tiny-claw/internal/schema"
|
||||
)
|
||||
|
||||
type ReadSkillTool struct {
|
||||
loader *prompt.SkillLoader
|
||||
}
|
||||
|
||||
func NewReadSkillTool(workDir string) *ReadSkillTool {
|
||||
return &ReadSkillTool{
|
||||
loader: prompt.NewSkillLoader(workDir),
|
||||
}
|
||||
}
|
||||
|
||||
func (t *ReadSkillTool) Name() string {
|
||||
return "read_skill"
|
||||
}
|
||||
|
||||
func (t *ReadSkillTool) Definition() schema.ToolDefinition {
|
||||
return schema.ToolDefinition{
|
||||
Name: t.Name(),
|
||||
Description: "按技能名称加载完整的技能指令。在开始任务前可先用 read_skill 获取该技能的详细执行指南。支持技能: git-workflow",
|
||||
InputSchema: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"name": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "要加载的技能名称,如 git-workflow",
|
||||
},
|
||||
},
|
||||
"required": []string{"name"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type readSkillArgs struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func (t *ReadSkillTool) Execute(ctx context.Context, args json.RawMessage) (string, error) {
|
||||
var input readSkillArgs
|
||||
if err := json.Unmarshal(args, &input); err != nil {
|
||||
return "", fmt.Errorf("参数解析失败: %w", err)
|
||||
}
|
||||
|
||||
skill, err := t.loader.Load(input.Name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return fmt.Sprintf("技能: %s\n描述: %s\n\n--- 技能正文 ---\n%s", skill.Name, skill.Description, skill.Body), nil
|
||||
}
|
||||
Reference in New Issue
Block a user