feat: 主聊天流程接入工具调用 — Synthesizer 支持 ChatWithTools

Synthesizer 现在向 LLM 传递工具定义并通过 ChatWithTools 执行
工具调用循环(最多5轮),执行结果通过 ToolRegistry 记录到调用
日志。Orchestrator 通过 SetToolRegistry() 注入。用户聊天现在可以
触发 web_search 等工具,调用记录在 DevTools 监控页面可见。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-26 21:30:30 +08:00
parent 12e9f7da6e
commit 251068a7db
3 changed files with 110 additions and 7 deletions
+1
View File
@@ -321,6 +321,7 @@ func main() {
memRetriever,
memExtractor,
)
orch.SetToolRegistry(toolRegistry)
log.Println("对话编排器 v2.0 已就绪")
_ = orch
@@ -17,6 +17,8 @@ import (
"github.com/yourname/cyrene-ai/ai-core/internal/bus"
"github.com/yourname/cyrene-ai/ai-core/internal/scheduler"
plgManager "github.com/yourname/cyrene-ai/pkg/plugins/manager"
)
// Orchestrator 对话编排器 v2.0
@@ -35,6 +37,7 @@ type Orchestrator struct {
enrichmentStore *SessionEnrichmentStore
msgScheduler *scheduler.MessageScheduler
emotionTracker *persona.EmotionTracker
toolRegistry *plgManager.ToolRegistry
}
// SetResponseCache sets the response cache (optional, for Phase 0.2).
@@ -62,6 +65,12 @@ func (o *Orchestrator) SetEmotionTracker(t *persona.EmotionTracker) {
o.emotionTracker = t
}
// SetToolRegistry sets the tool registry for tool-calling support in the main chat flow.
func (o *Orchestrator) SetToolRegistry(tr *plgManager.ToolRegistry) {
o.toolRegistry = tr
o.synthesizer.toolRegistry = tr
}
// getBus returns the bus or a nop fallback.
func (o *Orchestrator) getBus() bus.Bus {
if o.eventBus == nil {
@@ -87,7 +96,7 @@ func NewOrchestrator(
llmAdapter: chatAdapter,
subManager: subManager,
intentAnalyzer: NewIntentAnalyzer(intentAdapter),
synthesizer: NewSynthesizer(chatAdapter),
synthesizer: NewSynthesizer(chatAdapter, nil),
memoryRetriever: memoryRetriever,
memoryExtractor: memoryExtractor,
}
@@ -2,24 +2,29 @@ package orchestrator
import (
"context"
"encoding/json"
"fmt"
"github.com/yourname/cyrene-ai/pkg/logger"
"strings"
"github.com/yourname/cyrene-ai/ai-core/internal/llm"
"github.com/yourname/cyrene-ai/ai-core/internal/model"
"github.com/yourname/cyrene-ai/pkg/logger"
plgManager "github.com/yourname/cyrene-ai/pkg/plugins/manager"
plgSDK "github.com/yourname/cyrene-ai/pkg/plugins/sdk"
)
// Synthesizer 主会话综合器
// 汇总子会话结果,生成最终回复
type Synthesizer struct {
llmAdapter *llm.Adapter
llmAdapter *llm.Adapter
toolRegistry *plgManager.ToolRegistry
}
// NewSynthesizer 创建综合器
func NewSynthesizer(llmAdapter *llm.Adapter) *Synthesizer {
func NewSynthesizer(llmAdapter *llm.Adapter, toolRegistry *plgManager.ToolRegistry) *Synthesizer {
return &Synthesizer{
llmAdapter: llmAdapter,
llmAdapter: llmAdapter,
toolRegistry: toolRegistry,
}
}
@@ -46,8 +51,73 @@ func (s *Synthesizer) Synthesize(ctx context.Context, params SynthesizeParams) (
logger.Printf("[synthesizer] 开始综合 (上下文 %d 条消息)", len(messages))
// 流式调用 LLM
return s.llmAdapter.ChatStream(ctx, messages)
openAITools := s.buildOpenAITools()
if len(openAITools) == 0 {
return s.llmAdapter.ChatStream(ctx, messages)
}
resp, err := s.llmAdapter.ChatWithTools(ctx, messages, openAITools)
if err != nil {
logger.Printf("[synthesizer] ChatWithTools 失败: %v", err)
return nil, err
}
maxRounds := 5
for round := 0; len(resp.ToolCalls) > 0 && round < maxRounds; round++ {
logger.Printf("[synthesizer] LLM 请求 %d 个工具调用 (round=%d)", len(resp.ToolCalls), round)
messages = append(messages, model.LLMMessage{
Role: model.RoleAssistant,
Content: resp.Content,
ToolCalls: resp.ToolCalls,
ReasoningContent: resp.ReasoningContent,
})
for _, tc := range resp.ToolCalls {
var args map[string]interface{}
if err := json.Unmarshal([]byte(tc.Arguments), &args); err != nil {
logger.Printf("[synthesizer] 工具 %s 参数解析失败: %v", tc.Name, err)
args = make(map[string]interface{})
}
result, execErr := s.toolRegistry.Execute(ctx, tc.Name, args)
if execErr != nil {
logger.Printf("[synthesizer] 工具 %s 执行失败: %v", tc.Name, execErr)
}
if result == nil {
result = &plgSDK.ToolResult{ToolName: tc.Name, Success: false, Error: execErr.Error()}
}
resultJSON, _ := json.Marshal(result)
messages = append(messages, model.LLMMessage{
Role: model.RoleTool,
Content: string(resultJSON),
ToolCallID: tc.ID,
})
}
resp, err = s.llmAdapter.ChatWithTools(ctx, messages, openAITools)
if err != nil {
logger.Printf("[synthesizer] ChatWithTools 失败 (round=%d): %v", round+1, err)
return nil, err
}
}
finalContent := resp.Content
ch := make(chan llm.StreamChunk, 200)
go func() {
defer close(ch)
runes := []rune(finalContent)
for i := 0; i < len(runes); i += 3 {
end := i + 3
if end > len(runes) {
end = len(runes)
}
ch <- llm.StreamChunk{Content: string(runes[i:end])}
}
ch <- llm.StreamChunk{Done: true}
}()
return ch, nil
}
// buildSynthesizeMessages 构建综合用的 LLM 消息列表
@@ -119,6 +189,29 @@ func (s *Synthesizer) buildSynthesizeMessages(params SynthesizeParams) []model.L
return messages
}
// buildOpenAITools 将工具注册中心的定义转换为 LLM 工具格式
func (s *Synthesizer) buildOpenAITools() []llm.OpenAITool {
if s.toolRegistry == nil || !s.toolRegistry.IsEnabled() {
return nil
}
defs := s.toolRegistry.Definitions()
if len(defs) == 0 {
return nil
}
result := make([]llm.OpenAITool, 0, len(defs))
for _, d := range defs {
result = append(result, llm.OpenAITool{
Type: "function",
Function: llm.OpenAIToolFunc{
Name: d.Name,
Description: d.Description,
Parameters: d.Parameters,
},
})
}
return result
}
// AggregateResults 汇总子会话结果
func AggregateResults(results []model.SubSessionResult) *AggregatedContext {
agg := &AggregatedContext{