fix(llm): 工具路由配置修改后立即生效,不再硬编码 system prompt
toolrouter 每轮调用都从 DB 读取最新配置,/admin/llm/api 保存后下一条消息即生效, 无需重启。ai.NewService 启动时从 DB 加载初始配置,移除硬编码 prompt 覆盖用户配置的问题。 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+60
-7
@@ -2,6 +2,7 @@ package ai
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -12,6 +13,7 @@ import (
|
|||||||
"meshtastic_mqtt_server/internal/autoreply"
|
"meshtastic_mqtt_server/internal/autoreply"
|
||||||
"meshtastic_mqtt_server/internal/conversation"
|
"meshtastic_mqtt_server/internal/conversation"
|
||||||
"meshtastic_mqtt_server/internal/llm"
|
"meshtastic_mqtt_server/internal/llm"
|
||||||
|
storepkg "meshtastic_mqtt_server/internal/store"
|
||||||
"meshtastic_mqtt_server/internal/toolmanager"
|
"meshtastic_mqtt_server/internal/toolmanager"
|
||||||
"meshtastic_mqtt_server/internal/toolrouter"
|
"meshtastic_mqtt_server/internal/toolrouter"
|
||||||
|
|
||||||
@@ -24,6 +26,14 @@ type ToolConfigStore interface {
|
|||||||
GetLLMPrimaryConfigEnableTool() (bool, error)
|
GetLLMPrimaryConfigEnableTool() (bool, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ToolRouterStore 是 ai 服务依赖的 ToolRouter 持久化接口;
|
||||||
|
// 通常由 *store.Store 实现(GetLLMToolRouter)。
|
||||||
|
// 通过本接口我们可以让 toolrouter 在每轮调用时拉取最新配置,
|
||||||
|
// 让 /admin/llm/api 中的修改在保存后立即生效(无需重启)。
|
||||||
|
type ToolRouterStore interface {
|
||||||
|
GetLLMToolRouter() (*storepkg.LLMToolRouterRecord, error)
|
||||||
|
}
|
||||||
|
|
||||||
// Config holds the AI service configuration
|
// Config holds the AI service configuration
|
||||||
type Config struct {
|
type Config struct {
|
||||||
LLMProviders []llm.ProviderConfig
|
LLMProviders []llm.ProviderConfig
|
||||||
@@ -31,6 +41,7 @@ type Config struct {
|
|||||||
Enabled bool
|
Enabled bool
|
||||||
ConsoleLog bool
|
ConsoleLog bool
|
||||||
ToolConfigStore ToolConfigStore
|
ToolConfigStore ToolConfigStore
|
||||||
|
ToolRouterStore ToolRouterStore
|
||||||
}
|
}
|
||||||
|
|
||||||
// Service manages all AI-related components
|
// Service manages all AI-related components
|
||||||
@@ -45,6 +56,41 @@ type Service struct {
|
|||||||
enabled bool
|
enabled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// toolRouterConfigAdapter 把 ToolRouterStore 适配成 toolrouter.ConfigStore,
|
||||||
|
// 每次 LoadToolRouterConfig 都从 DB 拉取最新一行 llm_tool_router。
|
||||||
|
type toolRouterConfigAdapter struct {
|
||||||
|
store ToolRouterStore
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadToolRouterConfig 实现 toolrouter.ConfigStore。
|
||||||
|
// 当 DB 没有记录时返回 nil + nil,由 toolrouter 内部回退到内存默认值。
|
||||||
|
func (a *toolRouterConfigAdapter) LoadToolRouterConfig() (*toolrouter.Config, error) {
|
||||||
|
if a == nil || a.store == nil {
|
||||||
|
return nil, errors.New("tool router store is not configured")
|
||||||
|
}
|
||||||
|
record, err := a.store.GetLLMToolRouter()
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return toolRouterConfigFromRecord(record), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func toolRouterConfigFromRecord(r *storepkg.LLMToolRouterRecord) *toolrouter.Config {
|
||||||
|
if r == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &toolrouter.Config{
|
||||||
|
Enabled: r.Enabled,
|
||||||
|
OpenAIName: r.OpenAIName,
|
||||||
|
Timeout: r.Timeout,
|
||||||
|
MaxTokens: r.MaxTokens,
|
||||||
|
SystemPrompt: r.SystemPrompt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// NewService creates a new AI service
|
// NewService creates a new AI service
|
||||||
func NewService(cfg Config, db *gorm.DB, botSender autoreply.BotSender) (*Service, error) {
|
func NewService(cfg Config, db *gorm.DB, botSender autoreply.BotSender) (*Service, error) {
|
||||||
if !cfg.Enabled {
|
if !cfg.Enabled {
|
||||||
@@ -67,14 +113,21 @@ func NewService(cfg Config, db *gorm.DB, botSender autoreply.BotSender) (*Servic
|
|||||||
return nil, fmt.Errorf("failed to initialize LLM state: %w", err)
|
return nil, fmt.Errorf("failed to initialize LLM state: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize tool router
|
// 初始化 tool router:优先从 DB 读取已保存的配置,避免硬编码 prompt 把
|
||||||
toolRouterCfg := &toolrouter.Config{
|
// 用户在 /admin/llm/api 配置好的内容覆盖掉。
|
||||||
Enabled: true,
|
var (
|
||||||
Timeout: 30,
|
toolRouterCfg *toolrouter.Config
|
||||||
MaxTokens: 512,
|
toolRouterOptions []toolrouter.Option
|
||||||
SystemPrompt: "你是一个智能助手,可以调用工具来回答用户问题。\n用户正在通过 Mesh 网络与你对话,请保持回答简洁明了。\n工具结果优先于模型内置知识。",
|
)
|
||||||
|
if cfg.ToolRouterStore != nil {
|
||||||
|
adapter := &toolRouterConfigAdapter{store: cfg.ToolRouterStore}
|
||||||
|
// 启动时拉一次作为初始 cfg;失败或为空时让 toolrouter.NewState 走内置默认值。
|
||||||
|
if loaded, loadErr := adapter.LoadToolRouterConfig(); loadErr == nil && loaded != nil {
|
||||||
|
toolRouterCfg = loaded
|
||||||
}
|
}
|
||||||
toolRouter, err := toolrouter.NewState(toolRouterCfg, llmState)
|
toolRouterOptions = append(toolRouterOptions, toolrouter.WithConfigStore(adapter))
|
||||||
|
}
|
||||||
|
toolRouter, err := toolrouter.NewState(toolRouterCfg, llmState, toolRouterOptions...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to initialize tool router: %w", err)
|
return nil, fmt.Errorf("failed to initialize tool router: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,8 +60,8 @@ func RunAgentToolLoop(ctx context.Context, state *State, profile *llm.Profile, s
|
|||||||
if emit != nil {
|
if emit != nil {
|
||||||
emit(stream.Frame{Type: "trace", Tool: "agent_tools", Stage: "prepare", Status: "success", Message: "已准备可用工具", Data: map[string]any{"tools": availableNames, "tool_descriptions": toolDescriptions}})
|
emit(stream.Frame{Type: "trace", Tool: "agent_tools", Stage: "prepare", Status: "success", Message: "已准备可用工具", Data: map[string]any{"tools": availableNames, "tool_descriptions": toolDescriptions}})
|
||||||
}
|
}
|
||||||
if state == nil || state.cfg == nil {
|
if state == nil {
|
||||||
// No tool router config, but we have tools - use primary system prompt
|
// No tool router state, but we have tools - use primary system prompt
|
||||||
if strings.TrimSpace(systemPrompt) != "" {
|
if strings.TrimSpace(systemPrompt) != "" {
|
||||||
systemMessage := &model.ChatCompletionMessage{
|
systemMessage := &model.ChatCompletionMessage{
|
||||||
Role: "system",
|
Role: "system",
|
||||||
@@ -74,8 +74,10 @@ func RunAgentToolLoop(ctx context.Context, state *State, profile *llm.Profile, s
|
|||||||
}
|
}
|
||||||
return finalMessages, nil
|
return finalMessages, nil
|
||||||
}
|
}
|
||||||
|
// 每轮调用都重新加载最新配置,确保管理员在 /admin/llm/api 保存后立即生效
|
||||||
|
cfg := state.effectiveConfig()
|
||||||
// Use tool router system prompt if available, otherwise fall back to primary system prompt
|
// Use tool router system prompt if available, otherwise fall back to primary system prompt
|
||||||
prompt := strings.TrimSpace(state.cfg.SystemPrompt)
|
prompt := strings.TrimSpace(cfg.SystemPrompt)
|
||||||
if prompt == "" {
|
if prompt == "" {
|
||||||
prompt = strings.TrimSpace(systemPrompt)
|
prompt = strings.TrimSpace(systemPrompt)
|
||||||
}
|
}
|
||||||
@@ -96,11 +98,11 @@ func RunAgentToolLoop(ctx context.Context, state *State, profile *llm.Profile, s
|
|||||||
resp, err := completion.CompleteChat(ctx, routerProfile, model.CreateChatCompletionRequest{
|
resp, err := completion.CompleteChat(ctx, routerProfile, model.CreateChatCompletionRequest{
|
||||||
Model: routerProfile.Config.Model,
|
Model: routerProfile.Config.Model,
|
||||||
Messages: decisionMessages,
|
Messages: decisionMessages,
|
||||||
MaxTokens: &state.cfg.MaxTokens,
|
MaxTokens: &cfg.MaxTokens,
|
||||||
Tools: definitions,
|
Tools: definitions,
|
||||||
ToolChoice: model.ToolChoiceStringTypeAuto,
|
ToolChoice: model.ToolChoiceStringTypeAuto,
|
||||||
ParallelToolCalls: BoolPtr(false),
|
ParallelToolCalls: BoolPtr(false),
|
||||||
}, time.Duration(state.cfg.Timeout)*time.Second)
|
}, time.Duration(cfg.Timeout)*time.Second)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return finalMessages, err
|
return finalMessages, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,15 +17,30 @@ type Config struct {
|
|||||||
SystemPrompt string
|
SystemPrompt string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ConfigStore 定义从持久化层读取最新 ToolRouter 配置的能力。
|
||||||
|
// 每次 RunAgentToolLoop 都会调用 LoadToolRouterConfig,从而保证管理员
|
||||||
|
// 在 /admin/llm/api 修改配置后立即生效,无需重启。
|
||||||
|
type ConfigStore interface {
|
||||||
|
LoadToolRouterConfig() (*Config, error)
|
||||||
|
}
|
||||||
|
|
||||||
// State manages the tool router state
|
// State manages the tool router state
|
||||||
type State struct {
|
type State struct {
|
||||||
cfg *Config
|
cfg *Config
|
||||||
ai *llm.State
|
ai *llm.State
|
||||||
|
store ConfigStore
|
||||||
}
|
}
|
||||||
|
|
||||||
// Option is a function that configures the State
|
// Option is a function that configures the State
|
||||||
type Option func(*State)
|
type Option func(*State)
|
||||||
|
|
||||||
|
// WithConfigStore 注入运行时配置加载器,State 会在每次需要时拉取最新配置。
|
||||||
|
func WithConfigStore(store ConfigStore) Option {
|
||||||
|
return func(s *State) {
|
||||||
|
s.store = store
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// NewState creates a new tool router state
|
// NewState creates a new tool router state
|
||||||
func NewState(cfg *Config, ai *llm.State, options ...Option) (*State, error) {
|
func NewState(cfg *Config, ai *llm.State, options ...Option) (*State, error) {
|
||||||
if cfg == nil {
|
if cfg == nil {
|
||||||
@@ -51,12 +66,31 @@ func NewState(cfg *Config, ai *llm.State, options ...Option) (*State, error) {
|
|||||||
return state, nil
|
return state, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// effectiveConfig 返回当前生效的配置:优先从 store 加载最新值,加载失败时回退到内存 cfg。
|
||||||
|
// 调用方拿到的永远是非 nil 指针;内存 cfg 也保持同步以便其它读取点。
|
||||||
|
func (s *State) effectiveConfig() *Config {
|
||||||
|
if s == nil {
|
||||||
|
return &Config{}
|
||||||
|
}
|
||||||
|
if s.store != nil {
|
||||||
|
if latest, err := s.store.LoadToolRouterConfig(); err == nil && latest != nil {
|
||||||
|
s.cfg = latest
|
||||||
|
return latest
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if s.cfg == nil {
|
||||||
|
return &Config{}
|
||||||
|
}
|
||||||
|
return s.cfg
|
||||||
|
}
|
||||||
|
|
||||||
// RouterProfile returns the LLM profile configured for the tool router
|
// RouterProfile returns the LLM profile configured for the tool router
|
||||||
func (s *State) RouterProfile(fallback *llm.Profile) *llm.Profile {
|
func (s *State) RouterProfile(fallback *llm.Profile) *llm.Profile {
|
||||||
if s == nil || s.cfg == nil || s.ai == nil {
|
if s == nil || s.ai == nil {
|
||||||
return fallback
|
return fallback
|
||||||
}
|
}
|
||||||
name := strings.TrimSpace(s.cfg.OpenAIName)
|
cfg := s.effectiveConfig()
|
||||||
|
name := strings.TrimSpace(cfg.OpenAIName)
|
||||||
if name == "" {
|
if name == "" {
|
||||||
return fallback
|
return fallback
|
||||||
}
|
}
|
||||||
@@ -69,8 +103,8 @@ func (s *State) RouterProfile(fallback *llm.Profile) *llm.Profile {
|
|||||||
|
|
||||||
// Config returns a copy of the current configuration
|
// Config returns a copy of the current configuration
|
||||||
func (s *State) Config() Config {
|
func (s *State) Config() Config {
|
||||||
if s == nil || s.cfg == nil {
|
if s == nil {
|
||||||
return Config{}
|
return Config{}
|
||||||
}
|
}
|
||||||
return *s.cfg
|
return *s.effectiveConfig()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -422,6 +422,7 @@ func run(cfg *configpkg.Config) error {
|
|||||||
Enabled: cfg.AI.Enabled,
|
Enabled: cfg.AI.Enabled,
|
||||||
ConsoleLog: cfg.ConsoleLog.LLM,
|
ConsoleLog: cfg.ConsoleLog.LLM,
|
||||||
ToolConfigStore: store,
|
ToolConfigStore: store,
|
||||||
|
ToolRouterStore: store,
|
||||||
}, store.DB(), botSenderAdapter)
|
}, store.DB(), botSenderAdapter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Warning: failed to initialize AI service: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Warning: failed to initialize AI service: %v\n", err)
|
||||||
|
|||||||
Reference in New Issue
Block a user