Files
sundynix-agentix/sundynix-mcp-go/cmd/server/main.go
T
Blizzard 9a3a816c80 feat(prompts): prompt 版本化地基 —— 注册表 + 运行期文件覆盖
把散落各服务的硬编码 system prompt 收口为受管注册表,不重编译即可改/回滚/对比:
- shared/prompts:内置默认(随代码) + 运行期覆盖(PROMPTS_FILE) + Get/Keys,并发安全,含单测
- 接入 9 处:mcp-go(graph.extract);dispatcher(eval.quality/eval.refine/guard.jailbreak/
  coordinator.lead/memory.extract,按引用登记默认、无文本重复)
- main 启动调 LoadFile 加载 PROMPTS_FILE 覆盖
- live A/B:覆盖 graph.extract → 图谱抽取 2 条→0 条、向量仍正常(覆盖生效、管道未坏)
- v2(DB 控制面热切换 + 灰度)留后续

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 14:36:04 +08:00

115 lines
4.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.
// Command server 启动 sundynix-mcp-go —— 第 5 层 Go I/O 型 MCP 工具微服务。
package main
import (
"context"
"log"
"os"
"os/signal"
"syscall"
sharedbus "github.com/sundynix/sundynix-shared/bus"
"github.com/sundynix/sundynix-shared/contract"
"github.com/sundynix/sundynix-mcp-go/internal/history"
"github.com/sundynix/sundynix-mcp-go/internal/mcp"
"github.com/sundynix/sundynix-mcp-go/internal/memory"
"github.com/sundynix/sundynix-mcp-go/internal/rag"
"github.com/sundynix/sundynix-mcp-go/internal/search"
"github.com/sundynix/sundynix-shared/otelx"
"github.com/sundynix/sundynix-shared/prompts"
"github.com/sundynix/sundynix-shared/secrets"
)
func main() {
secrets.MustHaveKeyInProd() // 生产须配 SUNDYNIX_SECRET_KEY 以解密下发的 api_key 密文
otelx.SetupSlog("sundynix-mcp-go") // 结构化日志 + 链路感知(工具日志带 trace_id)
// 链路追踪:工具服务端 span 续上 dispatcher 的 tracetool.call → tool.serve)。
shutdownTrace, _ := otelx.Init(context.Background(), "sundynix-mcp-go")
defer func() { _ = shutdownTrace(context.Background()) }()
// 受管 prompt:登记内置默认 + 加载 PROMPTS_FILE 覆盖(不重编译即可改图谱抽取词)。
prompts.SetDefault(prompts.GraphExtract,
"你是知识图谱抽取器。从用户文本中抽取知识三元组,输出 JSON 数组,每项形如 {\"s\":\"主体\",\"p\":\"关系\",\"o\":\"客体\"}。实体用简洁名词,关系用简短动词短语。只输出 JSON,不要任何解释或代码块标记。")
prompts.LoadFile()
natsURL := envOr("NATS_URL", "nats://localhost:4222")
// DSN 用 127.0.0.1 而非 localhost:免 pgx 每连接 DNS 解析,避免高并发连接池扩容时 DNS 雪崩
// (见 LOAD_TEST_REPORT §4.3)。
pgDSN := envOr("POSTGRES_DSN", "postgres://sundynix:sundynix@127.0.0.1:5432/sundynix?sslmode=disable")
redisAddr := envOr("REDIS_ADDR", "localhost:6379")
milvusAddr := envOr("MILVUS_ADDR", "localhost:19530")
embBase := envOr("EMBED_BASE_URL", "") // OpenAI 兼容 embeddings 端点(空=向量检索降级)
embKey := envOr("EMBED_API_KEY", "")
embModel := envOr("EMBED_MODEL", "")
rerankBase := envOr("RERANK_BASE_URL", "") // DashScope 文本重排端点(空=不启用 rerank)
rerankKey := envOr("RERANK_API_KEY", "")
rerankModel := envOr("RERANK_MODEL", "")
neo4jURI := envOr("NEO4J_URI", "neo4j://localhost:7687") // GraphRAG 图谱(连不上则降级)
neo4jUser := envOr("NEO4J_USER", "neo4j")
neo4jPass := envOr("NEO4J_PASS", "sundynix")
b, err := sharedbus.Connect(natsURL)
if err != nil {
log.Fatalf("[mcp_go] nats connect: %v", err)
}
defer b.Close()
log.Printf("[mcp_go] connected %s", natsURL)
engine := search.NewHybrid() // LLM Wiki 混合检索:Bleve + Milvus + Neo4j
mem := memory.Open(pgDSN) // 偏好记忆:sundynix_user_profile(连不上则降级)
defer mem.Close()
hist := history.Open(redisAddr) // 会话短期历史:Redis(连不上则降级)
defer hist.Close()
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
// RAG 核心链:embedding + Milvus(向量) + Bleve(全文) + Neo4j(图谱) + 可选 rerank
ragEngine := rag.Open(ctx, rag.Config{
MilvusAddr: milvusAddr,
EmbedBase: embBase, EmbedKey: embKey, EmbedModel: embModel,
RerankBase: rerankBase, RerankKey: rerankKey, RerankModel: rerankModel,
Neo4jURI: neo4jURI, Neo4jUser: neo4jUser, Neo4jPass: neo4jPass,
})
defer ragEngine.Close()
// 配置控制面:取激活 embedding(向量) + chat(图谱抽取) 配置并订阅热更新。
applyEmbed := func(cfg *contract.ModelConfig) {
if cfg != nil {
ragEngine.SetEmbedding(cfg.BaseURL, cfg.APIKey, cfg.Model)
}
}
applyChat := func(cfg *contract.ModelConfig) {
if cfg != nil {
ragEngine.SetChat(cfg.BaseURL, cfg.APIKey, cfg.Model)
}
}
// 先订阅热更新(控制台改配置即生效)。
if _, err := b.SubscribeConfigUpdated(contract.ConfigKindChat, applyChat); err != nil {
log.Printf("[mcp_go] subscribe chat config: %v", err)
}
if _, err := b.SubscribeConfigUpdated(contract.ConfigKindEmbedding, applyEmbed); err != nil {
log.Printf("[mcp_go] subscribe embedding config: %v", err)
}
// 后台重试拉初始配置:容忍 gateway 晚于本服务启动(避免一次性扑空致 RAG 长期降级)。
go b.RequestConfigWithRetry(ctx, contract.ConfigKindEmbedding, applyEmbed)
go b.RequestConfigWithRetry(ctx, contract.ConfigKindChat, applyChat)
gw := mcp.NewGateway(b, engine, mem, hist, ragEngine, pgDSN)
log.Println("[mcp_go] serving MCP over sundynix.tools.go.* (Ctrl-C to quit)")
if err := gw.Serve(ctx); err != nil && err != context.Canceled {
log.Fatalf("[mcp_go] exit: %v", err)
}
}
func envOr(key, def string) string {
if v := os.Getenv(key); v != "" {
return v
}
return def
}