Files
go-tiny-claw/internal/engine/loop.go
T
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

198 lines
6.8 KiB
Go
Raw 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.
package engine
import (
"context"
"fmt"
"log"
"sync"
"go-tiny-claw/internal/prompt"
"go-tiny-claw/internal/provider"
"go-tiny-claw/internal/schema"
"go-tiny-claw/internal/tools"
)
// AgentEngine 是微型 OS 的核心驱动
type AgentEngine struct {
provider provider.LLMProvider
registry tools.Registry
// WorkDir (工作区): 借鉴 OpenClaw 的理念,Agent 必须有一个明确的物理边界
WorkDir string
EnableThinking bool // 【新增】慢思考模式开关
composer *prompt.PromptComposer
}
func NewAgentEngine(p provider.LLMProvider, r tools.Registry, workDir string, enableThinking bool) *AgentEngine {
return &AgentEngine{
provider: p,
registry: r,
WorkDir: workDir,
EnableThinking: enableThinking,
composer: prompt.NewPromptComposer(workDir),
}
}
func (e *AgentEngine) String() string {
return fmt.Sprintf("AgentEngine{workDir: %s, thinking: %v, registry: %s}", e.WorkDir, e.EnableThinking, e.registry)
}
// dumpMessages 打印当前上下文中的所有消息 (调试用)
func dumpMessages(msgs []schema.Message) {
for i, msg := range msgs {
content := msg.Content
// if len(content) > 80 {
// content = content[:80] + "..."
// }
log.Printf(" [%02d] %-10s | %s", i, msg.Role, content)
}
}
// dumpTools 打印当前可用的工具列表
func dumpTools(tools []schema.ToolDefinition) {
if len(tools) == 0 {
log.Printf(" [Tools] (无可用工具)\n")
return
}
for i, t := range tools {
log.Printf(" [Tool %d] %s — %s", i, t.Name, t.Description)
}
}
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{
systemMsg, // 注入动态组装的内核、AGENTS.md 与 Skills
{Role: schema.RoleUser, Content: userPrompt},
}
turnCount := 0
const maxTurns = 10
for {
turnCount++
if turnCount > maxTurns {
log.Printf("[Engine] 已达最大轮数 (%d),强制终止。\n", maxTurns)
break
}
log.Printf("\n========== [Turn %d] 开始 ==========\n", turnCount)
dumpMessages(contextHistory)
// 获取当前挂载的所有工具定义
availableTools := e.registry.GetAvailableTools()
dumpTools(availableTools)
// ====================================================================
// Phase 1: 慢思考阶段 (Thinking) - 仅第一轮执行初始规划
// ====================================================================
if e.EnableThinking && turnCount == 1 {
log.Println("[Engine][Phase 1] 剥夺工具访问权,强制进入慢思考与规划阶段...")
// 核心机制:传入的 availableTools 为 nil!
// 大模型看不到任何 JSON Schema,被迫只能输出纯文本的思考过程。
thinkResp, err := e.provider.Generate(ctx, contextHistory, nil)
if err != nil {
return fmt.Errorf("Thinking 阶段生成失败: %w", err)
}
// 如果模型输出了思考过程,我们将其作为 Assistant 消息追加到上下文中
if thinkResp.Content != "" {
fmt.Printf("🧠 [内部思考 Trace]: %s\n", thinkResp.Content)
contextHistory = append(contextHistory, *thinkResp)
}
// 插入过渡指令:让模型知道现在可以调用工具了
contextHistory = append(contextHistory, schema.Message{
Role: schema.RoleUser,
Content: "根据你的推理,现在请使用可用的工具来完成任务。执行具体行动。",
})
}
// ====================================================================
// Phase 2: 行动阶段 (Action) - 恢复工具,顺着规划执行
// ====================================================================
log.Println("[Engine][Phase 2] 恢复工具挂载,等待模型采取行动...")
// 此时的 contextHistory 中已经包含了上一阶段模型自己的 Thinking Trace + 过渡指令。
// 模型会顺着自己的逻辑,结合恢复的 availableTools 发起精准的工具调用。
actionResp, err := e.provider.Generate(ctx, contextHistory, availableTools)
if err != nil {
return fmt.Errorf("Action 阶段生成失败: %w", err)
}
contextHistory = append(contextHistory, *actionResp)
if actionResp.Content != "" {
fmt.Printf("🤖 [对外回复]: %s\n", actionResp.Content)
}
// ====================================================================
// 退出与执行逻辑 (与上一讲保持一致)
// ====================================================================
if len(actionResp.ToolCalls) == 0 {
log.Println("[Engine] 模型未请求调用工具,任务宣告完成。")
break
}
log.Printf("[Engine] 模型请求并发调用 %d 个工具...\n", len(actionResp.ToolCalls))
// 【核心改造开始】: 从串行 (Sequential) 演进为并行 (Parallel)
// 1. 预分配一个固定长度的切片,用于安全地存放各个并发工具的执行结果(Observation)
// 长度与 ToolCalls 的数量完全一致
observationMsgs := make([]schema.Message, len(actionResp.ToolCalls))
// 2. 声明 WaitGroup 用于阻塞等待所有协程完成
var wg sync.WaitGroup
// 3. 遍历模型请求的所有工具,为每一个工具单独 Fork 出一个 Goroutine
for i, toolCall := range actionResp.ToolCalls {
wg.Add(1) // 增加计数器
// 开启协程。注意:一定要将索引 i 和 toolCall 作为参数传入匿名函数,防止闭包变量捕获陷阱!
go func(idx int, call schema.ToolCall) {
defer wg.Done() // 协程结束时计数器减一
log.Printf(" -> [Go-%d] 🛠️ 触发并行执行: %s\n", idx, call.Name)
// 调用底层 Registry 执行工具(物理操作)
result := e.registry.Execute(ctx, call)
if result.IsError {
log.Printf(" -> [Go-%d] ❌ 工具执行报错: %s\n", idx, result.Output)
} else {
log.Printf(" -> [Go-%d] ✅ 工具执行成功 (返回 %d 字节)\n", idx, len(result.Output))
}
// 将执行结果封装为一条用户消息 (RoleUser)
obsMsg := schema.Message{
Role: schema.RoleUser,
Content: result.Output,
ToolCallID: call.ID,
}
// 【线程安全】: 由于每个 Goroutine 操作的是预分配切片的不同索引,
// 这里不需要加锁 (Mutex),性能极高!
observationMsgs[idx] = obsMsg
}(i, toolCall) // 闭包传参
}
// 4. Join 阻塞等待:主循环挂起,直到所有的并发协程全部执行完毕
wg.Wait()
log.Println("[Engine] 所有并发工具执行完毕,开始聚合观察结果 (Observation)...")
// 5. 聚合装填:将并行的结果,按照原本的顺序,一次性追加到上下文时间线中
// 这等价于 contextHistory = append(contextHistory, observationMsgs...)
for _, obs := range observationMsgs {
contextHistory = append(contextHistory, obs)
}
}
return nil
}