5736ad145e
在 v1(注册表+文件覆盖)上加 DB 管理层与热切换,镜像 model-config 控制面: - store: sundynix_prompt 表(key/version/content/active) + ActivePrompts/ListPrompts/ CreateVersion/Activate/Deactivate - 控制面: ServePrompts/RequestActivePrompts(+Retry)/PublishPromptsUpdated/SubscribePromptsUpdated; prompts.ApplyOverrides 整体替换覆盖集(DB 激活集为权威) - gateway API: GET/POST /api/v1/prompts、version/activate/deactivate;激活/撤销即广播 - dispatcher/mcp-go: 启动拉激活集 + 订阅热更新(不重启) - live: 建版本→激活→mcp-go 图谱抽取 2→0→回滚 2(全程不重启);deactivate 回退代码默认;版本可回溯 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
122 lines
3.8 KiB
Go
122 lines
3.8 KiB
Go
// Package prompts 是受管系统提示词的注册表:内置默认(随代码) + 运行期覆盖(文件/控制面)。
|
|
// 让各服务散落硬编码的 system prompt 集中、可不重编译地改/回滚/对比/灰度 —— prompt 版本化的地基。
|
|
package prompts
|
|
|
|
import (
|
|
"encoding/json"
|
|
"log"
|
|
"os"
|
|
"sort"
|
|
"sync"
|
|
)
|
|
|
|
// 受管 prompt 的稳定键(key 即版本化/覆盖的寻址单位)。
|
|
const (
|
|
GraphExtract = "graph.extract" // mcp-go:知识图谱三元组抽取
|
|
EvalQuality = "eval.quality" // dispatcher:回答质量评审(harness)
|
|
EvalRefine = "eval.refine" // dispatcher:低质回答自动修订
|
|
GuardJailbreak = "guard.jailbreak" // dispatcher:输入越狱/注入分类
|
|
CoordinatorLead = "coordinator.lead" // dispatcher:多智能体协调者(lead)
|
|
MemoryExtract = "memory.extract" // dispatcher:长期记忆对账
|
|
)
|
|
|
|
// Known 是平台受管的全部 prompt key(供管理端列出可配项;网关进程不登记默认,靠它枚举)。
|
|
var Known = []string{GraphExtract, EvalQuality, EvalRefine, GuardJailbreak, CoordinatorLead, MemoryExtract}
|
|
|
|
// Registry 持有默认与覆盖;Get 取覆盖优先、否则默认。并发安全。
|
|
type Registry struct {
|
|
mu sync.RWMutex
|
|
defaults map[string]string
|
|
overrides map[string]string
|
|
}
|
|
|
|
func New() *Registry {
|
|
return &Registry{defaults: map[string]string{}, overrides: map[string]string{}}
|
|
}
|
|
|
|
var global = New()
|
|
|
|
// SetDefault 登记某 key 的内置默认(基线版本,随代码走)。各服务启动时登记自己的 prompt。
|
|
func SetDefault(key, text string) { global.SetDefault(key, text) }
|
|
func (r *Registry) SetDefault(key, text string) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
r.defaults[key] = text
|
|
}
|
|
|
|
// SetOverride 运行期覆盖某 key(文件/控制面下发)。空串=清覆盖、回退默认。
|
|
func SetOverride(key, text string) { global.SetOverride(key, text) }
|
|
func (r *Registry) SetOverride(key, text string) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
if text == "" {
|
|
delete(r.overrides, key)
|
|
return
|
|
}
|
|
r.overrides[key] = text
|
|
}
|
|
|
|
// ApplyOverrides 用给定集合整体替换当前覆盖集(控制面热下发:DB 激活集为权威)。
|
|
// 空值条目忽略;传空 map 即清空所有覆盖、全回退内置默认。
|
|
func ApplyOverrides(m map[string]string) { global.ApplyOverrides(m) }
|
|
func (r *Registry) ApplyOverrides(m map[string]string) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
next := make(map[string]string, len(m))
|
|
for k, v := range m {
|
|
if v != "" {
|
|
next[k] = v
|
|
}
|
|
}
|
|
r.overrides = next
|
|
}
|
|
|
|
// Get 取生效内容:有覆盖用覆盖,否则用默认(都没有则空串)。
|
|
func Get(key string) string { return global.Get(key) }
|
|
func (r *Registry) Get(key string) string {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
if v, ok := r.overrides[key]; ok && v != "" {
|
|
return v
|
|
}
|
|
return r.defaults[key]
|
|
}
|
|
|
|
// Keys 返回所有已登记默认的 key(排序,供管理面/启动日志)。
|
|
func Keys() []string { return global.Keys() }
|
|
func (r *Registry) Keys() []string {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
ks := make([]string, 0, len(r.defaults))
|
|
for k := range r.defaults {
|
|
ks = append(ks, k)
|
|
}
|
|
sort.Strings(ks)
|
|
return ks
|
|
}
|
|
|
|
// LoadFile 从 JSON 文件({key:content})加载覆盖 —— env PROMPTS_FILE,启动时调一次。
|
|
// 不重编译即可改/回滚/对比 prompt;DB 控制面热切换是后续增量。
|
|
func LoadFile() {
|
|
path := os.Getenv("PROMPTS_FILE")
|
|
if path == "" {
|
|
return
|
|
}
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
log.Printf("[prompts] 读 PROMPTS_FILE=%s 失败(用默认): %v", path, err)
|
|
return
|
|
}
|
|
var m map[string]string
|
|
if err := json.Unmarshal(data, &m); err != nil {
|
|
log.Printf("[prompts] 解析 PROMPTS_FILE 失败(用默认): %v", err)
|
|
return
|
|
}
|
|
n := 0
|
|
for k, v := range m {
|
|
SetOverride(k, v)
|
|
n++
|
|
}
|
|
log.Printf("[prompts] 从 %s 加载 %d 条覆盖", path, n)
|
|
}
|