feat(prompt+cache): implement 10-section prompt template, true KB regulation recall, behavior rules and multi-level caching (P4)
This commit is contained in:
@@ -119,6 +119,10 @@ func (s *Service) DocGov() *docgov.GovernanceEngine {
|
||||
return s.docGov
|
||||
}
|
||||
|
||||
func (s *Service) KB() *kb.Client {
|
||||
return s.kb
|
||||
}
|
||||
|
||||
var SupportedTasks = squad.SupportedTasks
|
||||
|
||||
var SupportedStages = []StageMeta{
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"chat-agent-system/internal/ctxmgr"
|
||||
"chat-agent-system/internal/docgov"
|
||||
"chat-agent-system/internal/kb"
|
||||
"chat-agent-system/internal/squad"
|
||||
"chat-agent-system/internal/storage"
|
||||
)
|
||||
@@ -139,4 +140,5 @@ type BizdevService interface {
|
||||
ContextManager() *ctxmgr.ContextManager
|
||||
CAS() *storage.CAS
|
||||
DocGov() *docgov.GovernanceEngine
|
||||
KB() *kb.Client
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"chat-agent-system/internal/bizdev"
|
||||
"chat-agent-system/internal/ctxmgr"
|
||||
"chat-agent-system/internal/intent"
|
||||
"chat-agent-system/internal/kb"
|
||||
"chat-agent-system/internal/squad"
|
||||
)
|
||||
|
||||
@@ -83,6 +84,8 @@ type UserProjectContext struct {
|
||||
RewrittenQuery string `json:"-"`
|
||||
// RewriteRules 本轮命中的改写规则列表(如 ellipsis_completion, pronoun_resolution)
|
||||
RewriteRules []string `json:"-"`
|
||||
// RegulationHits 知识库法规与强标检索命中项(带 CiteKey 引用键,供对话层精准引用)
|
||||
RegulationHits []kb.HitItem `json:"-"`
|
||||
// UpgradeOffered 路由升级建议冷却标记(每会话至多主动建议一次,防打扰——对齐"参谋不强推"人设)
|
||||
UpgradeOffered bool `json:"-"`
|
||||
}
|
||||
|
||||
@@ -75,7 +75,9 @@ func (e *CoordinatorEngine) generateDialogueReply(
|
||||
5. 若用户询问受控文档驳回/退回原因或整改要点,必须紧扣本轮问题与已知驳回理由作答,禁止复述无关的上一轮技术长文。
|
||||
6. 不要声称已修改系统配置、用户身份或受控文档状态;不要伪造已执行的工作流结果。
|
||||
7. 只把最终答复写入 reply.md,使用中文,不要输出无关元数据。
|
||||
8. 【相关历史片段】与【项目基准与协同上下文】中涉及其他成员姓名、个人沟通习惯、私人事实的内容,仅可用于理解语境,不得在答复中复述、评价或转述给当前用户;引用项目事件只允许引用客观结论。`
|
||||
8. 事实与引用:涉及法规标准、项目数据、历史结论时,优先依据【参考知识】与【项目基准与协同上下文】作答并沿用〔引用键〕格式;两者未覆盖且无把握时,明确告知"暂无充分依据",严禁编造标准编号、条款内容或项目事实。
|
||||
9. 隐私与跨用户边界:【相关历史片段】与【项目基准与协同上下文】中涉及其他成员姓名、个人沟通习惯、私人事实的内容仅用于理解语境,不得在答复中复述、评价或转述给当前用户;引用项目事件仅限客观结论(如谁完成了什么交付物)。
|
||||
10. 身份一致性:始终以「项目助手」自称,不得扮演医生/监管审评员等法定角色给出诊疗或注册裁决性结论,法规与型检解读须提示"以官方发文与实际检测为准"。`
|
||||
|
||||
if st := stream.FromContext(ctx); st != nil {
|
||||
st.EmitPhase("dialogue_generating", "正在生成项目助手回复...", "assistant", 0, 0, 60)
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"chat-agent-system/internal/ctxmgr"
|
||||
"chat-agent-system/internal/docgov"
|
||||
"chat-agent-system/internal/intent"
|
||||
"chat-agent-system/internal/kb"
|
||||
"chat-agent-system/internal/squad"
|
||||
"chat-agent-system/internal/stream"
|
||||
)
|
||||
@@ -1377,12 +1378,79 @@ func (e *CoordinatorEngine) handleDocGovProposalDecide(ctx context.Context, cand
|
||||
|
||||
// handleQueryRegulations 处理强标与审评指导原则查询
|
||||
func (e *CoordinatorEngine) handleQueryRegulations(ctx context.Context, cand *intent.CandidateIntent, userCtx *UserProjectContext) (*CoordinationResult, error) {
|
||||
searchQuery := ""
|
||||
if userCtx != nil && userCtx.RewrittenQuery != "" {
|
||||
searchQuery = userCtx.RewrittenQuery
|
||||
} else if userCtx != nil && userCtx.TaskState != nil && userCtx.TaskState.TopicAnchor != "" {
|
||||
searchQuery = userCtx.TaskState.TopicAnchor
|
||||
} else if userCtx != nil {
|
||||
searchQuery = userCtx.Project.Name
|
||||
}
|
||||
if userCtx != nil && userCtx.Project.DeviceType != "" && !strings.Contains(searchQuery, userCtx.Project.DeviceType) {
|
||||
searchQuery += " " + userCtx.Project.DeviceType
|
||||
}
|
||||
|
||||
var hits []kb.HitItem
|
||||
if e != nil && e.bizdevSvc != nil && e.bizdevSvc.KB() != nil && strings.TrimSpace(searchQuery) != "" {
|
||||
cacheKey := ctxmgr.HashKey("regulatory_search", searchQuery)
|
||||
cache := ctxmgr.GetGlobalCache()
|
||||
if cached, ok := cache.Get(ctxmgr.LayerC1Search, cacheKey); ok {
|
||||
if ch, ok := cached.([]kb.HitItem); ok {
|
||||
hits = ch
|
||||
}
|
||||
} else {
|
||||
kbHits, err := e.bizdevSvc.KB().Search(ctx, searchQuery, kb.SearchOptions{
|
||||
Domains: []kb.KnowledgeDomain{kb.DomainRegulatory},
|
||||
Limit: 5,
|
||||
})
|
||||
if err == nil && len(kbHits) > 0 {
|
||||
hits = kbHits
|
||||
cache.Set(ctxmgr.LayerC1Search, cacheKey, hits, 30)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString(fmt.Sprintf("关于「%s」适用的国家标准与法规强标,我为您梳理如下:\n\n", userCtx.Project.Name))
|
||||
sb.WriteString("1. **通用安全标准**:GB 9706.1-2020《医用电气设备 第1部分:基本安全和基本性能的通用要求》;\n")
|
||||
sb.WriteString("2. **电磁兼容强标**:YY 9706.102-2021 / YY 0505-2012《医用电气设备 第1-2部分:基本安全和基本性能的通用要求 并列标准:电磁兼容》;\n")
|
||||
sb.WriteString("3. **专项性能指导原则**:需遵循 NMPA 医疗器械技术审评中心现行审评要点,并重点完善风险管理报告(依据 GB/T 42062-2020)。\n\n")
|
||||
sb.WriteString("如需查看具体条款对比或型检预估,请随时吩咐我。")
|
||||
projectName := "医疗器械项目"
|
||||
if userCtx != nil && userCtx.Project.Name != "" {
|
||||
projectName = userCtx.Project.Name
|
||||
}
|
||||
|
||||
if len(hits) > 0 {
|
||||
if userCtx != nil {
|
||||
userCtx.RegulationHits = hits
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("关于「%s」适用的国家标准与法规强标,权威依据梳理如下:\n\n", projectName))
|
||||
for i, h := range hits {
|
||||
cite := h.CiteKey
|
||||
if cite == "" {
|
||||
cite = fmt.Sprintf("〔%s〕", h.DocCode)
|
||||
}
|
||||
title := h.Title
|
||||
if title == "" {
|
||||
title = h.DocCode
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%d. **%s** %s\n", i+1, title, cite))
|
||||
if h.HeadingPath != "" {
|
||||
sb.WriteString(fmt.Sprintf(" - 条款章节: %s\n", h.HeadingPath))
|
||||
}
|
||||
if h.Content != "" {
|
||||
cText := strings.TrimSpace(h.Content)
|
||||
if len([]rune(cText)) > 150 {
|
||||
cText = string([]rune(cText)[:150]) + "..."
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf(" - 条款要点: %s\n", cText))
|
||||
}
|
||||
}
|
||||
sb.WriteString("\n上述标准与条款可作为型检与注册申报的权威技术支撑。如需深入比对专标或预估测试周期,请随时吩咐我。")
|
||||
} else {
|
||||
// 降级基准模板
|
||||
sb.WriteString(fmt.Sprintf("关于「%s」适用的国家标准与法规强标,我为您梳理如下:\n\n", projectName))
|
||||
sb.WriteString("1. **通用安全标准**:GB 9706.1-2020《医用电气设备 第1部分:基本安全和基本性能的通用要求》;\n")
|
||||
sb.WriteString("2. **电磁兼容强标**:YY 9706.102-2021 / YY 0505-2012《医用电气设备 第1-2部分:基本安全和基本性能的通用要求 并列标准:电磁兼容》;\n")
|
||||
sb.WriteString("3. **专项性能指导原则**:需遵循 NMPA 医疗器械技术审评中心现行审评要点,并重点完善风险管理报告(依据 GB/T 42062-2020)。\n\n")
|
||||
sb.WriteString("如需查看具体条款对比或型检预估,请随时吩咐我。")
|
||||
}
|
||||
|
||||
return &CoordinationResult{
|
||||
Status: StatusAccepted,
|
||||
|
||||
@@ -896,3 +896,38 @@ func TestCoordinatorEngine_ApproveRoleValidationAndFinalConfirmationCard(t *test
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoordinatorEngine_QueryRegulations_BaselineFallback(t *testing.T) {
|
||||
engine := coordinator.NewCoordinatorEngine(nil)
|
||||
|
||||
userCtx := &coordinator.UserProjectContext{
|
||||
Project: coordinator.ProjectInfo{
|
||||
Name: "4K超高清荧光内窥镜系统",
|
||||
DeviceType: "内窥镜",
|
||||
},
|
||||
RewrittenQuery: "4K超高清荧光内窥镜系统适用的国家标准和法规有哪些?",
|
||||
}
|
||||
|
||||
recRes := &intent.IntentRecognitionResult{
|
||||
TopIntent: &intent.CandidateIntent{
|
||||
Intent: intent.IntentQueryRegulations,
|
||||
Branch: intent.BranchNewIntentPreempt,
|
||||
},
|
||||
}
|
||||
|
||||
res, err := engine.ProcessIntent(context.Background(), recRes, userCtx)
|
||||
if err != nil {
|
||||
t.Fatalf("process intent failed: %v", err)
|
||||
}
|
||||
|
||||
if res.Status != coordinator.StatusAccepted {
|
||||
t.Errorf("expected status ACCEPTED, got %s", res.Status)
|
||||
}
|
||||
if !strings.Contains(res.NaturalReply, "GB 9706.1-2020") {
|
||||
t.Errorf("expected reply to contain GB 9706.1-2020, got: %s", res.NaturalReply)
|
||||
}
|
||||
if !strings.Contains(res.NaturalReply, "YY 9706.102-2021") {
|
||||
t.Errorf("expected reply to contain YY 9706.102-2021, got: %s", res.NaturalReply)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"chat-agent-system/internal/ctxmgr"
|
||||
)
|
||||
|
||||
// MemoryComposer 分层记忆编排器:遵循严格的 7+1 优先级自上而下组装对话 Prompt
|
||||
// MemoryComposer 分层记忆编排器:遵循严格的十段式模板结构自上而下组装对话 Prompt
|
||||
type MemoryComposer struct {
|
||||
MaxWindowTurns int
|
||||
}
|
||||
@@ -19,19 +19,20 @@ func NewMemoryComposer() *MemoryComposer {
|
||||
}
|
||||
}
|
||||
|
||||
// ComposeDialoguePrompt 依据分层优先级编排完整提示词
|
||||
// 注入优先级:
|
||||
// ComposeDialoguePrompt 依据结构化规范编排完整提示词
|
||||
// 布局结构 (自上而下):
|
||||
// 1. 【工作记忆】 task_state (DST) + PendingExpectation 挂起建议 + InteractiveCard 状态
|
||||
// 2. 【短期记忆】 滑动窗口近 M 轮(逐轮编号)
|
||||
// 3. 【压缩记忆】 早期轮次滚动摘要
|
||||
// 4. 【长期记忆-画像】 PersonalizedGuidance(工作偏好注入语)
|
||||
// 5. 【长期记忆-检索】 相关历史片段 TopK≤3
|
||||
// 6. 【知识注入】 法规 KB 命中 / 往期标杆案例参考
|
||||
// 7. 【项目上下文】 项目基准信息、待办线索、黑板事实(声明最高优先级)
|
||||
// 8. 【本轮用户】 用户本轮真实输入
|
||||
// 2. 【项目基准与协同上下文】 项目/用户/器械/阶段/待办/黑板事实(最高优先级声明)
|
||||
// 3. 【早期对话摘要】 压缩记忆(仅长会话出现)
|
||||
// 4. 【近期对话】 短期记忆(滑动窗口近 M 轮,逐轮编号 + 范围标注)
|
||||
// 5. 【相关历史片段】 按需语义检索 TopK≤3
|
||||
// 6. 【参考知识】 法规 KB 命中(带 CiteKey 引用键)/ 标杆案例参考
|
||||
// 7. 【用户偏好画像】 PersonalizedGuidance(沟通风格与重点关注)
|
||||
// 8. 【本轮用户】 原文 + (结合上下文理解完整诉求: …)双呈现
|
||||
// 9. 【答复要求】 优先级裁决、引用规范、隐私条款与输出格式
|
||||
func (mc *MemoryComposer) ComposeDialoguePrompt(userInput string, userCtx *UserProjectContext) string {
|
||||
if userCtx == nil {
|
||||
return fmt.Sprintf("【本轮用户】\n%s\n\n请结合上下文,以温和助理口吻作答,并写入 reply.md。", userInput)
|
||||
return fmt.Sprintf("【本轮用户】\n%s\n\n【答复要求】\n请结合上下文,以温和助理口吻作答,并写入 reply.md。", userInput)
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
@@ -63,76 +64,7 @@ func (mc *MemoryComposer) ComposeDialoguePrompt(userInput string, userCtx *UserP
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// 2. 【短期记忆】 滑动窗口近 M 轮 (~3000 字)
|
||||
// -------------------------------------------------------------
|
||||
if len(userCtx.RecentDialogue) > 0 {
|
||||
sb.WriteString("【近期对话】\n")
|
||||
turns := userCtx.RecentDialogue
|
||||
if len(turns) > mc.MaxWindowTurns {
|
||||
turns = turns[len(turns)-mc.MaxWindowTurns:]
|
||||
}
|
||||
for i, t := range turns {
|
||||
roleName := "用户"
|
||||
if t.Role == "assistant" {
|
||||
roleName = "助手"
|
||||
} else if t.Role == "system" {
|
||||
roleName = "系统"
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("[%d] %s: %s\n", i+1, roleName, t.Content))
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// 3. 【压缩记忆】 早期轮次滚动摘要 (~300 字)
|
||||
// -------------------------------------------------------------
|
||||
if s := strings.TrimSpace(userCtx.DialogueSummary); s != "" {
|
||||
sb.WriteString(s)
|
||||
if !strings.HasSuffix(s, "\n") {
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// 4. 【长期记忆-画像】 PersonalizedGuidance (~300 字)
|
||||
// -------------------------------------------------------------
|
||||
if g := strings.TrimSpace(userCtx.PersonalizedGuidance); g != "" {
|
||||
sb.WriteString(g)
|
||||
if !strings.HasSuffix(g, "\n") {
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// 5. 【长期记忆-检索】 相关历史片段 TopK≤3 (~600 字)
|
||||
// -------------------------------------------------------------
|
||||
recalledText := strings.TrimSpace(userCtx.RecalledMemorySnippet)
|
||||
if recalledText == "" && len(userCtx.RecalledMemories) > 0 {
|
||||
recalledText = strings.TrimSpace(ctxmgr.FormatRecalledMemorySnippet(userCtx.RecalledMemories, userCtx.SnapshotID))
|
||||
}
|
||||
if recalledText != "" {
|
||||
sb.WriteString(recalledText)
|
||||
if !strings.HasSuffix(recalledText, "\n") {
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// 6. 【知识注入】 法规 KB / 标杆案例参考 (仅特定意图, ~800 字)
|
||||
// -------------------------------------------------------------
|
||||
if userCtx.LatestAdvisorBrief != nil && len(userCtx.LatestAdvisorBrief.RecommendedCases) > 0 {
|
||||
sb.WriteString("【往期标杆案例参考】\n")
|
||||
for _, c := range userCtx.LatestAdvisorBrief.RecommendedCases {
|
||||
sb.WriteString(fmt.Sprintf("- 《%s》(%s): %s\n", c.ArtifactTitle, c.TaskKey, c.SummarySnippet))
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// 7. 【项目上下文】 项目基准信息、待办事项、黑板事实 (声明最高优先级)
|
||||
// 2. 【项目基准与协同上下文】 项目基准信息、待办事项、黑板事实 (声明最高优先级)
|
||||
// -------------------------------------------------------------
|
||||
sb.WriteString("【项目基准与协同上下文】\n")
|
||||
sb.WriteString(fmt.Sprintf("- 当前项目: %s (ID: %s)\n", userCtx.Project.Name, userCtx.Project.ID))
|
||||
@@ -176,6 +108,106 @@ func (mc *MemoryComposer) ComposeDialoguePrompt(userInput string, userCtx *UserP
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// 3. 【早期对话摘要】 压缩记忆 (~300 字)
|
||||
// -------------------------------------------------------------
|
||||
if s := strings.TrimSpace(userCtx.DialogueSummary); s != "" {
|
||||
sb.WriteString(s)
|
||||
if !strings.HasSuffix(s, "\n") {
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// 4. 【近期对话】 滑动窗口近 M 轮 (~3000 字,含范围标注)
|
||||
// -------------------------------------------------------------
|
||||
if len(userCtx.RecentDialogue) > 0 {
|
||||
totalTurns := len(userCtx.RecentDialogue)
|
||||
turns := userCtx.RecentDialogue
|
||||
startIdx := 1
|
||||
if totalTurns > mc.MaxWindowTurns {
|
||||
turns = turns[totalTurns-mc.MaxWindowTurns:]
|
||||
startIdx = totalTurns - mc.MaxWindowTurns + 1
|
||||
sb.WriteString(fmt.Sprintf("【近期对话】(第 %d~%d 轮,更早对话已压缩至摘要)\n", startIdx, totalTurns))
|
||||
} else {
|
||||
sb.WriteString("【近期对话】\n")
|
||||
}
|
||||
|
||||
for i, t := range turns {
|
||||
roleName := "用户"
|
||||
if t.Role == "assistant" {
|
||||
roleName = "助手"
|
||||
} else if t.Role == "system" {
|
||||
roleName = "系统"
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("[%d] %s: %s\n", startIdx+i, roleName, t.Content))
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// 5. 【相关历史片段】 语义检索切片 TopK≤3 (~600 字)
|
||||
// -------------------------------------------------------------
|
||||
recalledText := strings.TrimSpace(userCtx.RecalledMemorySnippet)
|
||||
if recalledText == "" && len(userCtx.RecalledMemories) > 0 {
|
||||
recalledText = strings.TrimSpace(ctxmgr.FormatRecalledMemorySnippet(userCtx.RecalledMemories, userCtx.SnapshotID))
|
||||
}
|
||||
if recalledText != "" {
|
||||
sb.WriteString(recalledText)
|
||||
if !strings.HasSuffix(recalledText, "\n") {
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// 6. 【参考知识】 法规 KB 命中(带 CiteKey)/ 往期标杆案例
|
||||
// -------------------------------------------------------------
|
||||
hasKnowledge := len(userCtx.RegulationHits) > 0 ||
|
||||
(userCtx.LatestAdvisorBrief != nil && len(userCtx.LatestAdvisorBrief.RecommendedCases) > 0)
|
||||
if hasKnowledge {
|
||||
sb.WriteString("【参考知识】(权威法规与标准依据,涉及结论时请沿用〔引用键〕引用)\n")
|
||||
for _, h := range userCtx.RegulationHits {
|
||||
cite := h.CiteKey
|
||||
if cite == "" {
|
||||
cite = fmt.Sprintf("〔%s〕", h.DocCode)
|
||||
}
|
||||
title := h.Title
|
||||
if title == "" {
|
||||
title = h.DocCode
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("• %s %s\n", cite, title))
|
||||
if h.HeadingPath != "" {
|
||||
sb.WriteString(fmt.Sprintf(" - 条款章节: %s\n", h.HeadingPath))
|
||||
}
|
||||
if h.Content != "" {
|
||||
cText := strings.TrimSpace(h.Content)
|
||||
if len([]rune(cText)) > 150 {
|
||||
cText = string([]rune(cText)[:150]) + "..."
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf(" - 条款要点: %s\n", cText))
|
||||
}
|
||||
}
|
||||
if userCtx.LatestAdvisorBrief != nil && len(userCtx.LatestAdvisorBrief.RecommendedCases) > 0 {
|
||||
for _, c := range userCtx.LatestAdvisorBrief.RecommendedCases {
|
||||
sb.WriteString(fmt.Sprintf("• 《%s》(%s): %s\n", c.ArtifactTitle, c.TaskKey, c.SummarySnippet))
|
||||
}
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// 7. 【用户偏好画像】 PersonalizedGuidance (~300 字)
|
||||
// -------------------------------------------------------------
|
||||
if g := strings.TrimSpace(userCtx.PersonalizedGuidance); g != "" {
|
||||
sb.WriteString(g)
|
||||
if !strings.HasSuffix(g, "\n") {
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// 8. 【本轮用户】
|
||||
// -------------------------------------------------------------
|
||||
@@ -185,7 +217,16 @@ func (mc *MemoryComposer) ComposeDialoguePrompt(userInput string, userCtx *UserP
|
||||
} else {
|
||||
sb.WriteString(userInput)
|
||||
}
|
||||
sb.WriteString("\n\n请结合上下文,以温和助理口吻作答,并写入 reply.md。")
|
||||
sb.WriteString("\n\n")
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// 9. 【答复要求】
|
||||
// -------------------------------------------------------------
|
||||
sb.WriteString("【答复要求】\n")
|
||||
sb.WriteString("1. 优先级裁决:项目黑板核心事实 > 本轮用户输入 > 近期对话 > 早期摘要 > 检索片段。\n")
|
||||
sb.WriteString("2. 事实与引用:涉及法规条款与标准时,优先依据【参考知识】与【项目基准与协同上下文】并沿用〔引用键〕格式;无把握时明确提示依据不足,严禁生造标准号。\n")
|
||||
sb.WriteString("3. 隐私与跨用户边界:其他成员姓名与私人习惯仅限理解语境,禁止外泄复述。\n")
|
||||
sb.WriteString("4. 答复目标:请结合上下文,以温和严谨的「项目助手」口吻作答,并写入 reply.md。")
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"chat-agent-system/internal/coordinator"
|
||||
"chat-agent-system/internal/ctxmgr"
|
||||
"chat-agent-system/internal/intent"
|
||||
"chat-agent-system/internal/kb"
|
||||
)
|
||||
|
||||
func TestMemoryComposer_HierarchyAndPriority(t *testing.T) {
|
||||
@@ -54,30 +55,45 @@ func TestMemoryComposer_HierarchyAndPriority(t *testing.T) {
|
||||
SimilarityScore: 0.91,
|
||||
},
|
||||
},
|
||||
RegulationHits: []kb.HitItem{
|
||||
{
|
||||
DocCode: "GB 9706.1-2020",
|
||||
Title: "医用电气设备 第1部分:基本安全和基本性能的通用要求",
|
||||
CiteKey: "〔GB 9706.1-2020 §8.4〕",
|
||||
HeadingPath: "8.4 保护接地",
|
||||
Content: "设备应当设置可靠的保护接地端子,满足接地阻抗要求。",
|
||||
},
|
||||
},
|
||||
BlackboardFacts: map[string]string{
|
||||
"budget": "150万",
|
||||
"device_type": "超声手术设备",
|
||||
},
|
||||
RewrittenQuery: "那「超声切割止血系统」后续的型检送检如何安排?",
|
||||
}
|
||||
|
||||
prompt := composer.ComposeDialoguePrompt("那后续的型检送检如何安排?", userCtx)
|
||||
|
||||
// 验证包含各个优先级分层标记
|
||||
// 验证包含各个结构化分层标记
|
||||
sections := []string{
|
||||
"【工作记忆】",
|
||||
"【本轮任务状态 (Working Memory)】",
|
||||
"- 上一轮挂起建议摘要: 建议补充型检预估周期",
|
||||
"【项目基准与协同上下文】",
|
||||
"项目黑板核心事实(最高优先级,与早期摘要或历史片段冲突时以此为准)",
|
||||
"【早期对话摘要】",
|
||||
"【近期对话】",
|
||||
"[1] 用户: 这个项目的预算是多少?",
|
||||
"[2] 助手: 一期预算建议控制在150万以内。",
|
||||
"【早期对话摘要】",
|
||||
"【用户偏好建议】",
|
||||
"【相关历史片段】",
|
||||
"[本会话·第2轮]",
|
||||
"【项目基准与协同上下文】",
|
||||
"项目黑板核心事实(最高优先级,与早期摘要或历史片段冲突时以此为准)",
|
||||
"【参考知识】",
|
||||
"〔GB 9706.1-2020 §8.4〕",
|
||||
"【用户偏好建议】",
|
||||
"【本轮用户】",
|
||||
"那后续的型检送检如何安排?",
|
||||
"用户原话: 那后续的型检送检如何安排?",
|
||||
"(结合上下文理解完整诉求: 那「超声切割止血系统」后续的型检送检如何安排?)",
|
||||
"【答复要求】",
|
||||
"优先级裁决:项目黑板核心事实 > 本轮用户输入 > 近期对话 > 早期摘要 > 检索片段。",
|
||||
}
|
||||
|
||||
for _, sec := range sections {
|
||||
@@ -86,23 +102,27 @@ func TestMemoryComposer_HierarchyAndPriority(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// 验证层级相对顺序(自上而下):
|
||||
// 工作记忆 < 近期对话 < 早期对话摘要 < 用户偏好建议 < 相关历史片段 < 项目基准与协同上下文 < 本轮用户
|
||||
// 验证严格的自上而下布局顺序:
|
||||
// 工作记忆 < 项目上下文 < 早期摘要 < 近期对话 < 相关历史片段 < 参考知识 < 用户偏好 < 本轮用户 < 答复要求
|
||||
idxWorkMem := strings.Index(prompt, "【工作记忆】")
|
||||
idxRecent := strings.Index(prompt, "【近期对话】")
|
||||
idxSummary := strings.Index(prompt, "【早期对话摘要】")
|
||||
idxGuidance := strings.Index(prompt, "【用户偏好建议】")
|
||||
idxRecalled := strings.Index(prompt, "【相关历史片段】")
|
||||
idxProject := strings.Index(prompt, "【项目基准与协同上下文】")
|
||||
idxSummary := strings.Index(prompt, "【早期对话摘要】")
|
||||
idxRecent := strings.Index(prompt, "【近期对话】")
|
||||
idxRecalled := strings.Index(prompt, "【相关历史片段】")
|
||||
idxReference := strings.Index(prompt, "【参考知识】")
|
||||
idxGuidance := strings.Index(prompt, "【用户偏好建议】")
|
||||
idxUser := strings.Index(prompt, "【本轮用户】")
|
||||
idxReq := strings.Index(prompt, "【答复要求】")
|
||||
|
||||
if !(idxWorkMem < idxRecent &&
|
||||
idxRecent < idxSummary &&
|
||||
idxSummary < idxGuidance &&
|
||||
idxGuidance < idxRecalled &&
|
||||
idxRecalled < idxProject &&
|
||||
idxProject < idxUser) {
|
||||
t.Errorf("prompt section ordering violation! indices: workMem=%d, recent=%d, summary=%d, guidance=%d, recalled=%d, project=%d, user=%d",
|
||||
idxWorkMem, idxRecent, idxSummary, idxGuidance, idxRecalled, idxProject, idxUser)
|
||||
if !(idxWorkMem < idxProject &&
|
||||
idxProject < idxSummary &&
|
||||
idxSummary < idxRecent &&
|
||||
idxRecent < idxRecalled &&
|
||||
idxRecalled < idxReference &&
|
||||
idxReference < idxGuidance &&
|
||||
idxGuidance < idxUser &&
|
||||
idxUser < idxReq) {
|
||||
t.Errorf("prompt section ordering violation! indices: workMem=%d, project=%d, summary=%d, recent=%d, recalled=%d, ref=%d, guidance=%d, user=%d, req=%d",
|
||||
idxWorkMem, idxProject, idxSummary, idxRecent, idxRecalled, idxReference, idxGuidance, idxUser, idxReq)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
package ctxmgr
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 缓存层级常量定义
|
||||
const (
|
||||
LayerC1Search = "C1" // 检索缓存 (kb.Search / SearchChatMemory), 默认 TTL 10m
|
||||
LayerC2Rewrite = "C2" // 改写缓存 (RewriteResult), 默认 TTL 30m
|
||||
LayerC3Embedding = "C3" // 文本向量化缓存, 默认 TTL 60m
|
||||
LayerC4Context = "C4" // 上下文组装产物, 默认 TTL 5m (黑板/任务状态更新主动失效)
|
||||
LayerC5Summary = "C5" // 会话滚动摘要, 默认 TTL 5m (摘要生成主动失效)
|
||||
|
||||
DefaultMaxEntriesPerLayer = 1000
|
||||
)
|
||||
|
||||
type cacheEntry struct {
|
||||
value any
|
||||
expiresAt time.Time
|
||||
costMs int64
|
||||
hits int64
|
||||
}
|
||||
|
||||
// CacheStats 缓存运行监控指标
|
||||
type CacheStats struct {
|
||||
Hits int64 `json:"hits"`
|
||||
Misses int64 `json:"misses"`
|
||||
SavedMs int64 `json:"saved_ms"`
|
||||
HitRate float64 `json:"hit_rate"`
|
||||
}
|
||||
|
||||
// MultiLevelCache 统一 LRU+TTL 内存多级缓存系统
|
||||
type MultiLevelCache struct {
|
||||
mu sync.RWMutex
|
||||
layers map[string]map[string]*cacheEntry
|
||||
defaultTTLs map[string]time.Duration
|
||||
maxEntries int
|
||||
enabled bool
|
||||
totalHits atomic.Int64
|
||||
totalMisses atomic.Int64
|
||||
totalSavedMs atomic.Int64
|
||||
}
|
||||
|
||||
var globalCache *MultiLevelCache
|
||||
var globalCacheOnce sync.Once
|
||||
|
||||
// GetGlobalCache 获取全局多级缓存单例
|
||||
func GetGlobalCache() *MultiLevelCache {
|
||||
globalCacheOnce.Do(func() {
|
||||
globalCache = NewMultiLevelCache()
|
||||
})
|
||||
return globalCache
|
||||
}
|
||||
|
||||
// NewMultiLevelCache 创建多级缓存实例
|
||||
func NewMultiLevelCache() *MultiLevelCache {
|
||||
enabled := true
|
||||
if env := strings.ToLower(strings.TrimSpace(os.Getenv("CACHE_ENABLED"))); env == "false" || env == "0" || env == "off" {
|
||||
enabled = false
|
||||
}
|
||||
|
||||
return &MultiLevelCache{
|
||||
layers: map[string]map[string]*cacheEntry{
|
||||
LayerC1Search: make(map[string]*cacheEntry),
|
||||
LayerC2Rewrite: make(map[string]*cacheEntry),
|
||||
LayerC3Embedding: make(map[string]*cacheEntry),
|
||||
LayerC4Context: make(map[string]*cacheEntry),
|
||||
LayerC5Summary: make(map[string]*cacheEntry),
|
||||
},
|
||||
defaultTTLs: map[string]time.Duration{
|
||||
LayerC1Search: 10 * time.Minute,
|
||||
LayerC2Rewrite: 30 * time.Minute,
|
||||
LayerC3Embedding: 60 * time.Minute,
|
||||
LayerC4Context: 5 * time.Minute,
|
||||
LayerC5Summary: 5 * time.Minute,
|
||||
},
|
||||
maxEntries: DefaultMaxEntriesPerLayer,
|
||||
enabled: enabled,
|
||||
}
|
||||
}
|
||||
|
||||
// SetEnabled 动态控制缓存开关(用于测试或运行时降级)
|
||||
func (c *MultiLevelCache) SetEnabled(enabled bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.enabled = enabled
|
||||
}
|
||||
|
||||
// IsEnabled 检查缓存是否启用
|
||||
func (c *MultiLevelCache) IsEnabled() bool {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return c.enabled
|
||||
}
|
||||
|
||||
// HashKey 计算多字段 SHA256 字符串指纹
|
||||
func HashKey(parts ...string) string {
|
||||
h := sha256.New()
|
||||
for _, p := range parts {
|
||||
h.Write([]byte(p))
|
||||
h.Write([]byte("|"))
|
||||
}
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
// Get 读取指定层级的缓存
|
||||
func (c *MultiLevelCache) Get(layer string, key string) (any, bool) {
|
||||
if c == nil || !c.IsEnabled() || key == "" {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
c.mu.RLock()
|
||||
layerMap, ok := c.layers[layer]
|
||||
if !ok {
|
||||
c.mu.RUnlock()
|
||||
c.totalMisses.Add(1)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
entry, found := layerMap[key]
|
||||
if !found {
|
||||
c.mu.RUnlock()
|
||||
c.totalMisses.Add(1)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if time.Now().After(entry.expiresAt) {
|
||||
c.mu.RUnlock()
|
||||
// 惰性驱逐
|
||||
c.Invalidate(layer, key)
|
||||
c.totalMisses.Add(1)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
c.mu.RUnlock()
|
||||
|
||||
entry.hits++
|
||||
c.totalHits.Add(1)
|
||||
if entry.costMs > 0 {
|
||||
c.totalSavedMs.Add(entry.costMs)
|
||||
}
|
||||
return entry.value, true
|
||||
}
|
||||
|
||||
// Set 写入指定层级的缓存
|
||||
func (c *MultiLevelCache) Set(layer string, key string, val any, costMs int64) {
|
||||
if c == nil || !c.IsEnabled() || key == "" || val == nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
layerMap, ok := c.layers[layer]
|
||||
if !ok {
|
||||
layerMap = make(map[string]*cacheEntry)
|
||||
c.layers[layer] = layerMap
|
||||
}
|
||||
|
||||
// 容量保护:超过单层最大容量时清除超限旧条目
|
||||
if len(layerMap) >= c.maxEntries {
|
||||
now := time.Now()
|
||||
for k, v := range layerMap {
|
||||
if now.After(v.expiresAt) {
|
||||
delete(layerMap, k)
|
||||
}
|
||||
}
|
||||
if len(layerMap) >= c.maxEntries {
|
||||
// 若无过期则随机驱逐10%
|
||||
n := 0
|
||||
for k := range layerMap {
|
||||
delete(layerMap, k)
|
||||
n++
|
||||
if n >= c.maxEntries/10 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ttl := c.defaultTTLs[layer]
|
||||
if ttl <= 0 {
|
||||
ttl = 10 * time.Minute
|
||||
}
|
||||
|
||||
layerMap[key] = &cacheEntry{
|
||||
value: val,
|
||||
expiresAt: time.Now().Add(ttl),
|
||||
costMs: costMs,
|
||||
hits: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// Invalidate 失效指定层级的某一个 Key
|
||||
func (c *MultiLevelCache) Invalidate(layer string, key string) {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if layerMap, ok := c.layers[layer]; ok {
|
||||
delete(layerMap, key)
|
||||
}
|
||||
}
|
||||
|
||||
// InvalidateLayer 整体清空某个层级(如黑板更新时清空 C4)
|
||||
func (c *MultiLevelCache) InvalidateLayer(layer string) {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if _, ok := c.layers[layer]; ok {
|
||||
c.layers[layer] = make(map[string]*cacheEntry)
|
||||
}
|
||||
}
|
||||
|
||||
// Clear 清空全部层级与统计
|
||||
func (c *MultiLevelCache) Clear() {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
for l := range c.layers {
|
||||
c.layers[l] = make(map[string]*cacheEntry)
|
||||
}
|
||||
c.totalHits.Store(0)
|
||||
c.totalMisses.Store(0)
|
||||
c.totalSavedMs.Store(0)
|
||||
}
|
||||
|
||||
// Stats 获取缓存性能与命中统计指标
|
||||
func (c *MultiLevelCache) Stats() CacheStats {
|
||||
if c == nil {
|
||||
return CacheStats{}
|
||||
}
|
||||
hits := c.totalHits.Load()
|
||||
misses := c.totalMisses.Load()
|
||||
saved := c.totalSavedMs.Load()
|
||||
rate := 0.0
|
||||
if total := hits + misses; total > 0 {
|
||||
rate = float64(hits) / float64(total)
|
||||
}
|
||||
return CacheStats{
|
||||
Hits: hits,
|
||||
Misses: misses,
|
||||
SavedMs: saved,
|
||||
HitRate: rate,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package ctxmgr_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"chat-agent-system/internal/ctxmgr"
|
||||
)
|
||||
|
||||
func TestMultiLevelCache_BasicOpsAndStats(t *testing.T) {
|
||||
cache := ctxmgr.NewMultiLevelCache()
|
||||
cache.Clear()
|
||||
|
||||
key := ctxmgr.HashKey("test_user", "test_query")
|
||||
|
||||
// 1. 首次读取未命中
|
||||
val, ok := cache.Get(ctxmgr.LayerC1Search, key)
|
||||
if ok || val != nil {
|
||||
t.Fatalf("expected cache miss, got %v", val)
|
||||
}
|
||||
|
||||
// 2. 写入并读取命中
|
||||
cache.Set(ctxmgr.LayerC1Search, key, "result_items", 45)
|
||||
val, ok = cache.Get(ctxmgr.LayerC1Search, key)
|
||||
if !ok || val.(string) != "result_items" {
|
||||
t.Fatalf("expected cache hit with 'result_items', got %v", val)
|
||||
}
|
||||
|
||||
// 3. 校验统计指标
|
||||
stats := cache.Stats()
|
||||
if stats.Hits != 1 || stats.Misses != 1 {
|
||||
t.Errorf("unexpected stats: hits=%d, misses=%d", stats.Hits, stats.Misses)
|
||||
}
|
||||
if stats.SavedMs != 45 {
|
||||
t.Errorf("expected savedMs=45, got %d", stats.SavedMs)
|
||||
}
|
||||
if stats.HitRate != 0.5 {
|
||||
t.Errorf("expected hitRate=0.5, got %f", stats.HitRate)
|
||||
}
|
||||
|
||||
// 4. 单条 Key 失效
|
||||
cache.Invalidate(ctxmgr.LayerC1Search, key)
|
||||
_, ok = cache.Get(ctxmgr.LayerC1Search, key)
|
||||
if ok {
|
||||
t.Errorf("expected cache miss after invalidation")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiLevelCache_LayerInvalidation(t *testing.T) {
|
||||
cache := ctxmgr.NewMultiLevelCache()
|
||||
cache.Clear()
|
||||
|
||||
k1 := ctxmgr.HashKey("u1", "p1")
|
||||
k2 := ctxmgr.HashKey("u2", "p2")
|
||||
|
||||
cache.Set(ctxmgr.LayerC4Context, k1, "ctx_1", 20)
|
||||
cache.Set(ctxmgr.LayerC4Context, k2, "ctx_2", 20)
|
||||
|
||||
// 整体清空 C4 层
|
||||
cache.InvalidateLayer(ctxmgr.LayerC4Context)
|
||||
|
||||
if _, ok := cache.Get(ctxmgr.LayerC4Context, k1); ok {
|
||||
t.Errorf("expected k1 in C4 to be cleared")
|
||||
}
|
||||
if _, ok := cache.Get(ctxmgr.LayerC4Context, k2); ok {
|
||||
t.Errorf("expected k2 in C4 to be cleared")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiLevelCache_DisabledToggle(t *testing.T) {
|
||||
cache := ctxmgr.NewMultiLevelCache()
|
||||
cache.SetEnabled(false)
|
||||
|
||||
key := ctxmgr.HashKey("k_disabled")
|
||||
cache.Set(ctxmgr.LayerC2Rewrite, key, "rewritten", 10)
|
||||
|
||||
if _, ok := cache.Get(ctxmgr.LayerC2Rewrite, key); ok {
|
||||
t.Errorf("expected cache to be inactive when disabled")
|
||||
}
|
||||
|
||||
cache.SetEnabled(true)
|
||||
cache.Set(ctxmgr.LayerC2Rewrite, key, "rewritten", 10)
|
||||
if _, ok := cache.Get(ctxmgr.LayerC2Rewrite, key); !ok {
|
||||
t.Errorf("expected cache to be active when enabled")
|
||||
}
|
||||
}
|
||||
@@ -199,6 +199,21 @@ func (m *ContextManager) SearchChatMemory(ctx context.Context, opts ChatMemorySe
|
||||
opts.MinScore = 0.25
|
||||
}
|
||||
|
||||
cacheKey := HashKey(
|
||||
"chat_memory",
|
||||
fmt.Sprintf("%d", opts.UserID),
|
||||
fmt.Sprintf("%d", opts.ProjectID),
|
||||
opts.Query,
|
||||
opts.SessionID,
|
||||
opts.ExcludeSessionID,
|
||||
fmt.Sprintf("%d", opts.TopK),
|
||||
)
|
||||
if cached, ok := m.Cache().Get(LayerC1Search, cacheKey); ok {
|
||||
if items, ok := cached.([]RecalledMemoryItem); ok {
|
||||
return items, nil
|
||||
}
|
||||
}
|
||||
|
||||
emb := m.embedder
|
||||
if emb == nil {
|
||||
emb = NewDeterministicEmbedder(1024)
|
||||
@@ -277,6 +292,7 @@ func (m *ContextManager) SearchChatMemory(ctx context.Context, opts ChatMemorySe
|
||||
if len(candidates) > opts.TopK {
|
||||
candidates = candidates[:opts.TopK]
|
||||
}
|
||||
m.Cache().Set(LayerC1Search, cacheKey, candidates, 20)
|
||||
return candidates, nil
|
||||
}
|
||||
|
||||
@@ -348,6 +364,7 @@ func (m *ContextManager) SearchChatMemory(ctx context.Context, opts ChatMemorySe
|
||||
if len(candidates) > opts.TopK {
|
||||
candidates = candidates[:opts.TopK]
|
||||
}
|
||||
m.Cache().Set(LayerC1Search, cacheKey, candidates, 20)
|
||||
return candidates, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ type ContextManager struct {
|
||||
activities map[int64][]ActivityItem // project_id -> activities
|
||||
memoryChunks []MemoryChunkItem // 语义记忆切片缓存
|
||||
summaries map[string]*ChatSummary // session_id -> rolling summary
|
||||
cache *MultiLevelCache // 多级统一缓存系统 (C1-C5)
|
||||
nextActivityID int64
|
||||
nextMessageID int64
|
||||
nextChunkID int64
|
||||
@@ -52,12 +53,24 @@ func NewContextManager(pool *pgxpool.Pool, cas *storage.CAS, decayCfg TimeDecayC
|
||||
activities: make(map[int64][]ActivityItem),
|
||||
memoryChunks: make([]MemoryChunkItem, 0),
|
||||
summaries: make(map[string]*ChatSummary),
|
||||
cache: NewMultiLevelCache(),
|
||||
nextActivityID: 1,
|
||||
nextMessageID: 1,
|
||||
nextChunkID: 1,
|
||||
}
|
||||
}
|
||||
|
||||
// Cache 获取上下文管理器的多级缓存实例
|
||||
func (m *ContextManager) Cache() *MultiLevelCache {
|
||||
if m == nil {
|
||||
return GetGlobalCache()
|
||||
}
|
||||
if m.cache == nil {
|
||||
m.cache = GetGlobalCache()
|
||||
}
|
||||
return m.cache
|
||||
}
|
||||
|
||||
// SetEmbedder 设置自定义向量嵌入器 (如生产环境 bge-m3)
|
||||
func (m *ContextManager) SetEmbedder(emb VectorEmbedder) {
|
||||
if emb != nil {
|
||||
@@ -375,6 +388,7 @@ func (m *ContextManager) UpdateProjectBlackboard(ctx context.Context, projectID
|
||||
return fmt.Errorf("upsert project blackboard failed: %w", err)
|
||||
}
|
||||
}
|
||||
m.Cache().InvalidateLayer(LayerC4Context)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,13 @@ func (m *ContextManager) GetChatSummary(ctx context.Context, sessionID string) (
|
||||
return nil, fmt.Errorf("sessionID required")
|
||||
}
|
||||
|
||||
if cached, ok := m.Cache().Get(LayerC5Summary, sessionID); ok {
|
||||
if sum, ok := cached.(*ChatSummary); ok && sum != nil {
|
||||
copySum := *sum
|
||||
return ©Sum, nil
|
||||
}
|
||||
}
|
||||
|
||||
if m.pool != nil {
|
||||
var sum ChatSummary
|
||||
var factsJSON, openQuestionsJSON []byte
|
||||
@@ -61,6 +68,7 @@ func (m *ContextManager) GetChatSummary(ctx context.Context, sessionID string) (
|
||||
if err == nil {
|
||||
_ = json.Unmarshal(factsJSON, &sum.Facts)
|
||||
_ = json.Unmarshal(openQuestionsJSON, &sum.OpenQuestions)
|
||||
m.Cache().Set(LayerC5Summary, sessionID, &sum, 15)
|
||||
return &sum, nil
|
||||
}
|
||||
}
|
||||
@@ -71,6 +79,7 @@ func (m *ContextManager) GetChatSummary(ctx context.Context, sessionID string) (
|
||||
if s, ok := m.summaries[sessionID]; ok && s != nil {
|
||||
// 返回副本
|
||||
copySum := *s
|
||||
m.Cache().Set(LayerC5Summary, sessionID, ©Sum, 15)
|
||||
return ©Sum, nil
|
||||
}
|
||||
}
|
||||
@@ -84,6 +93,10 @@ func (m *ContextManager) SaveChatSummary(ctx context.Context, sum *ChatSummary)
|
||||
}
|
||||
sum.UpdatedAt = time.Now()
|
||||
|
||||
// 主动失效 C5 摘要缓存与 C4 上下文缓存
|
||||
m.Cache().Invalidate(LayerC5Summary, sum.SessionID)
|
||||
m.Cache().InvalidateLayer(LayerC4Context)
|
||||
|
||||
if m.pool != nil {
|
||||
factsJSON, _ := json.Marshal(sum.Facts)
|
||||
openQuestionsJSON, _ := json.Marshal(sum.OpenQuestions)
|
||||
|
||||
Reference in New Issue
Block a user