feat(service): 实现单用户/项目多级上下文模型、时间衰减与用户习惯偏好管理

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
WeiCHEN
2026-09-13 02:50:19 +00:00
committed by weichen
co-authored by Cursor
parent 4de6c12721
commit 96be8fdd94
5 changed files with 961 additions and 0 deletions
+63
View File
@@ -0,0 +1,63 @@
package ctxmgr
import (
"math"
"time"
)
// CalculateDecayedWeight 计算考虑时间半衰期与里程碑豁免后的有效权重
func CalculateDecayedWeight(rawWeight float64, createdAt time.Time, isMilestone bool, cfg TimeDecayConfig) float64 {
if rawWeight <= 0 {
rawWeight = 1.00
}
if !cfg.Enabled {
return rawWeight
}
// 里程碑事件豁免衰减,作为永久基石
if isMilestone && cfg.MilestoneExempt {
return rawWeight
}
halfLife := cfg.HalfLifeDays
if halfLife <= 0 {
halfLife = 7.0
}
daysDiff := time.Since(createdAt).Hours() / 24.0
if daysDiff <= 0 {
return rawWeight
}
lambda := math.Ln2 / halfLife
decayFactor := math.Exp(-lambda * daysDiff)
decayed := rawWeight * decayFactor
// 应用衰减下限保护,防止早期有价值信息完全被截断
floor := rawWeight * cfg.MinWeightFloor
if decayed < floor {
decayed = floor
}
return math.Round(decayed*1000) / 1000
}
// ApplyTimeDecayToActivities 对活动流批量应用时间衰减并按最终有效权重重排
func ApplyTimeDecayToActivities(items []ActivityItem, cfg TimeDecayConfig) []ActivityItem {
res := make([]ActivityItem, len(items))
for i, it := range items {
it.DecayedWeight = CalculateDecayedWeight(it.RawWeight, it.CreatedAt, it.IsMilestone, cfg)
res[i] = it
}
// 稳定排序:按 DecayedWeight 降序,相同时按时间倒序
for i := 0; i < len(res); i++ {
for j := i + 1; j < len(res); j++ {
if res[j].DecayedWeight > res[i].DecayedWeight ||
(res[j].DecayedWeight == res[i].DecayedWeight && res[j].CreatedAt.After(res[i].CreatedAt)) {
res[i], res[j] = res[j], res[i]
}
}
}
return res
}
+431
View File
@@ -0,0 +1,431 @@
package ctxmgr
import (
"context"
"encoding/json"
"errors"
"fmt"
"sync"
"time"
"chat-agent-system/internal/storage"
"github.com/jackc/pgx/v5/pgxpool"
)
// ContextManager 多级与多类型上下文总管理器
type ContextManager struct {
pool *pgxpool.Pool
cas *storage.CAS
reflector *PreferenceReflector
decayCfg TimeDecayConfig
// 离线降级内存缓存
mu sync.RWMutex
users map[int64]*UserPreference
projects map[int64]*ProjectCollectiveContext
messages map[string][]ChatMessageRecord // session_id -> messages
activities map[int64][]ActivityItem // project_id -> activities
nextActivityID int64
nextMessageID int64
}
// NewContextManager 创建上下文管理器
func NewContextManager(pool *pgxpool.Pool, cas *storage.CAS, decayCfg TimeDecayConfig) *ContextManager {
if decayCfg.HalfLifeDays <= 0 {
decayCfg = DefaultTimeDecayConfig()
}
return &ContextManager{
pool: pool,
cas: cas,
reflector: NewPreferenceReflector(),
decayCfg: decayCfg,
users: make(map[int64]*UserPreference),
projects: make(map[int64]*ProjectCollectiveContext),
messages: make(map[string][]ChatMessageRecord),
activities: make(map[int64][]ActivityItem),
nextActivityID: 1,
nextMessageID: 1,
}
}
// RecordUserMessage 记录单轮对话消息并自动驱动工作习惯偏好更新
func (m *ContextManager) RecordUserMessage(ctx context.Context, sessionID string, userID int64, role, content, intent, branch string, meta map[string]any) error {
if sessionID == "" || content == "" {
return errors.New("sessionID and content required")
}
rec := ChatMessageRecord{
SessionID: sessionID,
UserID: userID,
Role: role,
Content: content,
Intent: intent,
Branch: branch,
Metadata: meta,
CreatedAt: time.Now(),
}
if m.pool != nil {
// 确保会话存在
_, _ = m.pool.Exec(ctx, `
INSERT INTO chat_sessions (session_id, user_id, title, created_at, updated_at)
VALUES ($1, $2, '对话协同会话', now(), now())
ON CONFLICT (session_id) DO UPDATE SET updated_at = now()
`, sessionID, userID)
metaJSON, _ := json.Marshal(meta)
_, err := m.pool.Exec(ctx, `
INSERT INTO chat_messages (session_id, user_id, role, content, intent, branch, metadata, created_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, now())
`, sessionID, userID, role, content, intent, branch, metaJSON)
if err != nil {
return fmt.Errorf("insert chat message failed: %w", err)
}
} else {
m.mu.Lock()
rec.ID = m.nextMessageID
m.nextMessageID++
m.messages[sessionID] = append(m.messages[sessionID], rec)
m.mu.Unlock()
}
// 若为用户输入,触发工作习惯偏好异步更新
if role == "user" && userID > 0 {
_ = m.ReflectUserPreference(ctx, userID, sessionID)
}
return nil
}
// RecordActivity 记录项目协同操作/决策/上传/下载活动
func (m *ContextManager) RecordActivity(ctx context.Context, projectID, userID int64, userName, actType, title string, detail map[string]any, weight float64, isMilestone bool) (*ActivityItem, error) {
if projectID <= 0 || title == "" {
return nil, errors.New("projectID and title required")
}
if weight <= 0 {
weight = 1.00
}
item := &ActivityItem{
ProjectID: projectID,
UserID: userID,
UserName: userName,
ActivityType: actType,
Title: title,
Detail: detail,
RawWeight: weight,
IsMilestone: isMilestone,
CreatedAt: time.Now(),
}
if m.pool != nil {
detailJSON, _ := json.Marshal(detail)
var actID int64
var createdAt time.Time
err := m.pool.QueryRow(ctx, `
INSERT INTO project_activities (project_id, user_id, user_name, activity_type, title, detail, weight, is_milestone, created_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, now())
RETURNING id, created_at
`, projectID, userID, userName, actType, title, detailJSON, weight, isMilestone).Scan(&actID, &createdAt)
if err != nil {
return nil, fmt.Errorf("insert activity failed: %w", err)
}
item.ID = actID
item.CreatedAt = createdAt
} else {
m.mu.Lock()
item.ID = m.nextActivityID
m.nextActivityID++
m.activities[projectID] = append(m.activities[projectID], *item)
m.mu.Unlock()
}
item.DecayedWeight = CalculateDecayedWeight(item.RawWeight, item.CreatedAt, item.IsMilestone, m.decayCfg)
return item, nil
}
// GetUserContext 装配单用户私有上下文 (包含个人偏好画像、近期会话、个人操作与经时间衰减的文件资料)
func (m *ContextManager) GetUserContext(ctx context.Context, projectID, userID int64, sessionID string) (*SingleUserContext, error) {
pref, _ := m.GetUserPreference(ctx, userID)
userCtx := &SingleUserContext{
UserID: userID,
Preference: pref,
SessionID: sessionID,
RecentChats: make([]ChatMessageRecord, 0),
MyActivities: make([]ActivityItem, 0),
MyFiles: make([]*storage.FileReference, 0),
AssembledAt: time.Now(),
}
// 1. 获取近期会话
if sessionID != "" {
if m.pool != nil {
rows, err := m.pool.Query(ctx, `
SELECT id, session_id, user_id, role, content, intent, branch, metadata, created_at
FROM chat_messages
WHERE session_id = $1
ORDER BY created_at ASC LIMIT 30
`, sessionID)
if err == nil {
defer rows.Close()
for rows.Next() {
var r ChatMessageRecord
var metaJSON []byte
if err := rows.Scan(&r.ID, &r.SessionID, &r.UserID, &r.Role, &r.Content, &r.Intent, &r.Branch, &metaJSON, &r.CreatedAt); err == nil {
_ = json.Unmarshal(metaJSON, &r.Metadata)
userCtx.RecentChats = append(userCtx.RecentChats, r)
}
}
}
} else {
m.mu.RLock()
userCtx.RecentChats = append(userCtx.RecentChats, m.messages[sessionID]...)
m.mu.RUnlock()
}
}
// 2. 获取用户个人在项目内的活动并应用时间衰减
var rawActs []ActivityItem
if m.pool != nil {
rows, err := m.pool.Query(ctx, `
SELECT id, project_id, user_id, user_name, activity_type, title, detail, weight, is_milestone, created_at
FROM project_activities
WHERE project_id = $1 AND user_id = $2
ORDER BY created_at DESC LIMIT 50
`, projectID, userID)
if err == nil {
defer rows.Close()
for rows.Next() {
var it ActivityItem
var detJSON []byte
if err := rows.Scan(&it.ID, &it.ProjectID, &it.UserID, &it.UserName, &it.ActivityType, &it.Title, &detJSON, &it.RawWeight, &it.IsMilestone, &it.CreatedAt); err == nil {
_ = json.Unmarshal(detJSON, &it.Detail)
rawActs = append(rawActs, it)
}
}
}
} else {
m.mu.RLock()
for _, it := range m.activities[projectID] {
if it.UserID == userID {
rawActs = append(rawActs, it)
}
}
m.mu.RUnlock()
}
userCtx.MyActivities = ApplyTimeDecayToActivities(rawActs, m.decayCfg)
return userCtx, nil
}
// GetProjectContext 装配项目级多方协同上下文 (黑板事实、里程碑决策、团队活动流与共享文件)
func (m *ContextManager) GetProjectContext(ctx context.Context, projectID int64) (*ProjectCollectiveContext, error) {
pCtx := &ProjectCollectiveContext{
ProjectID: projectID,
BlackboardFacts: make(map[string]string),
AllActivities: make([]ActivityItem, 0),
MilestoneDecisions: make([]ActivityItem, 0),
SharedFiles: make([]*storage.FileReference, 0),
ParticipatingUsers: make([]UserSummary, 0),
AssembledAt: time.Now(),
}
if m.pool != nil {
// 1. 获取项目基准与黑板事实
var pCode, pName, cStage string
_ = m.pool.QueryRow(ctx, "SELECT code, name, current_stage FROM projects WHERE id = $1", projectID).Scan(&pCode, &pName, &cStage)
pCtx.ProjectCode = pCode
pCtx.ProjectName = pName
pCtx.CurrentStage = cStage
var factsJSON []byte
err := m.pool.QueryRow(ctx, "SELECT facts FROM project_blackboards WHERE project_id = $1", projectID).Scan(&factsJSON)
if err == nil {
_ = json.Unmarshal(factsJSON, &pCtx.BlackboardFacts)
}
// 2. 获取全项目活动
rows, err := m.pool.Query(ctx, `
SELECT id, project_id, user_id, user_name, activity_type, title, detail, weight, is_milestone, created_at
FROM project_activities
WHERE project_id = $1
ORDER BY created_at DESC LIMIT 100
`, projectID)
if err == nil {
defer rows.Close()
var rawActs []ActivityItem
for rows.Next() {
var it ActivityItem
var detJSON []byte
if err := rows.Scan(&it.ID, &it.ProjectID, &it.UserID, &it.UserName, &it.ActivityType, &it.Title, &detJSON, &it.RawWeight, &it.IsMilestone, &it.CreatedAt); err == nil {
_ = json.Unmarshal(detJSON, &it.Detail)
rawActs = append(rawActs, it)
if it.IsMilestone || it.ActivityType == "decision" {
pCtx.MilestoneDecisions = append(pCtx.MilestoneDecisions, it)
}
}
}
pCtx.AllActivities = ApplyTimeDecayToActivities(rawActs, m.decayCfg)
}
} else {
m.mu.RLock()
if existing, ok := m.projects[projectID]; ok {
pCtx.ProjectName = existing.ProjectName
pCtx.CurrentStage = existing.CurrentStage
for k, v := range existing.BlackboardFacts {
pCtx.BlackboardFacts[k] = v
}
}
rawActs := m.activities[projectID]
for _, it := range rawActs {
if it.IsMilestone || it.ActivityType == "decision" {
pCtx.MilestoneDecisions = append(pCtx.MilestoneDecisions, it)
}
}
pCtx.AllActivities = ApplyTimeDecayToActivities(rawActs, m.decayCfg)
m.mu.RUnlock()
}
return pCtx, nil
}
// BuildBlendedSquadContext 混合上下文装配引擎 (Context Blender)
// 将项目事实、里程碑决策、触发用户的工作习惯偏好合成最适合目标 Agent 角色执行的混合上下文
func (m *ContextManager) BuildBlendedSquadContext(ctx context.Context, projectID, triggerUserID int64, taskKey, role string) (*BlendedSquadContext, error) {
projCtx, err := m.GetProjectContext(ctx, projectID)
if err != nil {
return nil, fmt.Errorf("get project context failed: %w", err)
}
userPref, _ := m.GetUserPreference(ctx, triggerUserID)
guidance := m.reflector.GeneratePromptGuidance(userPref)
var decisions []string
for _, d := range projCtx.MilestoneDecisions {
decisions = append(decisions, fmt.Sprintf("[%s] %s (%s)", d.ActivityType, d.Title, d.UserName))
}
return &BlendedSquadContext{
ProjectName: projCtx.ProjectName,
CurrentStage: projCtx.CurrentStage,
TaskKey: taskKey,
AgentRole: role,
BlackboardFacts: projCtx.BlackboardFacts,
KeyDecisions: decisions,
TriggerUserPreference: userPref,
PersonalizedGuidance: guidance,
AssembledAt: time.Now(),
}, nil
}
// GetUserPreference 获取用户偏好画像
func (m *ContextManager) GetUserPreference(ctx context.Context, userID int64) (*UserPreference, error) {
if userID <= 0 {
return nil, errors.New("invalid userID")
}
if m.pool != nil {
var pref UserPreference
var focusArr []string
var factsJSON []byte
err := m.pool.QueryRow(ctx, `
SELECT u.id, u.account_id, u.name, u.role, p.communication_style, p.focus_areas, p.output_format_pref, p.custom_instructions, p.memory_facts, p.updated_at
FROM users u
LEFT JOIN user_preferences p ON u.id = p.user_id
WHERE u.id = $1
`, userID).Scan(&pref.UserID, &pref.AccountID, &pref.Name, &pref.Role, &pref.CommunicationStyle, &focusArr, &pref.OutputFormatPref, &pref.CustomInstructions, &factsJSON, &pref.UpdatedAt)
if err == nil {
pref.FocusAreas = focusArr
_ = json.Unmarshal(factsJSON, &pref.MemoryFacts)
if pref.CommunicationStyle == "" {
pref.CommunicationStyle = StyleConciseStructured
}
return &pref, nil
}
}
m.mu.RLock()
defer m.mu.RUnlock()
if pref, ok := m.users[userID]; ok {
return pref, nil
}
return &UserPreference{
UserID: userID,
CommunicationStyle: StyleConciseStructured,
FocusAreas: []string{"reg_compliance", "timeline"},
OutputFormatPref: "standard_report",
MemoryFacts: make(map[string]string),
UpdatedAt: time.Now(),
}, nil
}
// UpdateUserPreference 保存用户偏好画像
func (m *ContextManager) UpdateUserPreference(ctx context.Context, pref *UserPreference) error {
if pref == nil || pref.UserID <= 0 {
return errors.New("invalid user preference")
}
pref.UpdatedAt = time.Now()
if m.pool != nil {
factsJSON, _ := json.Marshal(pref.MemoryFacts)
_, err := m.pool.Exec(ctx, `
INSERT INTO user_preferences (user_id, communication_style, focus_areas, output_format_pref, custom_instructions, memory_facts, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, now())
ON CONFLICT (user_id) DO UPDATE SET
communication_style = EXCLUDED.communication_style,
focus_areas = EXCLUDED.focus_areas,
output_format_pref = EXCLUDED.output_format_pref,
custom_instructions = EXCLUDED.custom_instructions,
memory_facts = EXCLUDED.memory_facts,
updated_at = now()
`, pref.UserID, string(pref.CommunicationStyle), pref.FocusAreas, pref.OutputFormatPref, pref.CustomInstructions, factsJSON)
if err != nil {
return fmt.Errorf("upsert user preference failed: %w", err)
}
}
m.mu.Lock()
m.users[pref.UserID] = pref
m.mu.Unlock()
return nil
}
// ReflectUserPreference 根据用户在指定会话的历史记录智能总结工作习惯与偏好
func (m *ContextManager) ReflectUserPreference(ctx context.Context, userID int64, sessionID string) error {
var msgs []ChatMessageRecord
if m.pool != nil {
rows, err := m.pool.Query(ctx, `
SELECT id, session_id, user_id, role, content, intent, branch, metadata, created_at
FROM chat_messages
WHERE user_id = $1 OR session_id = $2
ORDER BY created_at DESC LIMIT 20
`, userID, sessionID)
if err == nil {
defer rows.Close()
for rows.Next() {
var r ChatMessageRecord
var metaJSON []byte
if err := rows.Scan(&r.ID, &r.SessionID, &r.UserID, &r.Role, &r.Content, &r.Intent, &r.Branch, &metaJSON, &r.CreatedAt); err == nil {
_ = json.Unmarshal(metaJSON, &r.Metadata)
msgs = append(msgs, r)
}
}
}
} else {
m.mu.RLock()
msgs = append(msgs, m.messages[sessionID]...)
m.mu.RUnlock()
}
if len(msgs) == 0 {
return nil
}
currentPref, _ := m.GetUserPreference(ctx, userID)
updatedPref := m.reflector.ReflectFromMessages(msgs, currentPref)
return m.UpdateUserPreference(ctx, updatedPref)
}
+205
View File
@@ -0,0 +1,205 @@
package ctxmgr
import (
"context"
"os"
"strings"
"testing"
"time"
"github.com/jackc/pgx/v5/pgxpool"
)
func TestContextManager_PreferenceReflection(t *testing.T) {
mgr := NewContextManager(nil, nil, DefaultTimeDecayConfig())
ctx := context.Background()
userID := int64(99)
sessionID := "sess_pref_test"
// 模拟用户发送了多次体现其工作习惯的消息
_ = mgr.RecordUserMessage(ctx, sessionID, userID, "user", "说重点,太长了不要废话,直接给结论。", "QUERY_PROGRESS", "PREEMPT", nil)
_ = mgr.RecordUserMessage(ctx, sessionID, userID, "user", "本次申报重点关注国内NMPA与欧洲CE合规,另外严格把控工期排期和成本预算。", "SUPPLEMENT_INFO", "EXPECTATION_HIT", nil)
_ = mgr.RecordUserMessage(ctx, sessionID, userID, "user", "文末请务必附带适合向高管汇报的 PPT 要点清单。", "SUPPLEMENT_INFO", "EXPECTATION_HIT", nil)
pref, err := mgr.GetUserPreference(ctx, userID)
if err != nil {
t.Fatalf("get user preference failed: %v", err)
}
// 1. 验证沟通风格是否被提炼为精炼结构化
if pref.CommunicationStyle != StyleConciseStructured {
t.Errorf("expected StyleConciseStructured, got %s", pref.CommunicationStyle)
}
// 2. 验证关注领域是否包含合规、预算和周期
hasReg := false
hasCost := false
hasTimeline := false
for _, f := range pref.FocusAreas {
if f == "reg_compliance" {
hasReg = true
}
if f == "cost_budget" {
hasCost = true
}
if f == "timeline" {
hasTimeline = true
}
}
if !hasReg || !hasCost || !hasTimeline {
t.Errorf("missing focus areas in preference: %v", pref.FocusAreas)
}
// 3. 验证输出格式偏好
if pref.OutputFormatPref != "ppt_bullets" {
t.Errorf("expected ppt_bullets, got %s", pref.OutputFormatPref)
}
// 4. 验证生成的个性化 Prompt 指导语
guidance := mgr.reflector.GeneratePromptGuidance(pref)
if !strings.Contains(guidance, "结构精炼、结论先行") {
t.Errorf("guidance missing concise style instruction: %s", guidance)
}
if !strings.Contains(guidance, "法规标准合规与审评审批风险") {
t.Errorf("guidance missing reg compliance focus: %s", guidance)
}
if !strings.Contains(guidance, "PPT 的条目清单") {
t.Errorf("guidance missing ppt format instruction: %s", guidance)
}
}
func TestContextManager_TimeDecayActivities(t *testing.T) {
mgr := NewContextManager(nil, nil, TimeDecayConfig{
Enabled: true,
HalfLifeDays: 7.0,
MinWeightFloor: 0.10,
MilestoneExempt: true,
})
ctx := context.Background()
projectID := int64(888)
// 记录一项里程碑决策 (不衰减)
milestone, _ := mgr.RecordActivity(ctx, projectID, 1, "张工", "decision", "通过V0.5阶段可行性初审", nil, 1.0, true)
// 记录一项 14 天前的日常协同讨论 (经历 2 个半衰期)
oldOp, _ := mgr.RecordActivity(ctx, projectID, 2, "李工", "operation", "日常测试参数备选记录", nil, 1.0, false)
oldOp.CreatedAt = time.Now().Add(-14 * 24 * time.Hour)
// 更新内存中的活动列表时间以供测试排序
mgr.mu.Lock()
for i := range mgr.activities[projectID] {
if mgr.activities[projectID][i].ID == oldOp.ID {
mgr.activities[projectID][i].CreatedAt = oldOp.CreatedAt
}
}
mgr.mu.Unlock()
pCtx, err := mgr.GetProjectContext(ctx, projectID)
if err != nil {
t.Fatalf("get project context failed: %v", err)
}
if len(pCtx.AllActivities) < 2 {
t.Fatalf("expected 2 activities, got %d", len(pCtx.AllActivities))
}
// 里程碑事件排在最前且权重维持 1.0
if pCtx.AllActivities[0].ID != milestone.ID {
t.Errorf("expected milestone to rank highest, got ID %d", pCtx.AllActivities[0].ID)
}
if pCtx.AllActivities[0].DecayedWeight != 1.0 {
t.Errorf("expected milestone weight 1.0, got %.2f", pCtx.AllActivities[0].DecayedWeight)
}
// 14 天前的旧操作权重应衰减至 0.25 左右
if pCtx.AllActivities[1].DecayedWeight > 0.30 {
t.Errorf("expected old op decayed below 0.30, got %.3f", pCtx.AllActivities[1].DecayedWeight)
}
}
func TestContextManager_DatabaseIntegration(t *testing.T) {
dsn := os.Getenv("CONTEXT_DB_DSN")
if dsn == "" {
dsn = "postgres://app:dev_secret_db_pass@127.0.0.1:5433/agent_system_dev?sslmode=disable"
}
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
pool, err := pgxpool.New(ctx, dsn)
if err != nil {
t.Skipf("cannot connect to context db (%v), skipping DB test", err)
return
}
defer pool.Close()
if err := pool.Ping(ctx); err != nil {
t.Skipf("context db ping failed (%v), skipping DB test", err)
return
}
mgr := NewContextManager(pool, nil, DefaultTimeDecayConfig())
// 准备真实项目与用户
var projID int64
projCode := "PROJ_MGR_" + time.Now().Format("20060102150405")
err = pool.QueryRow(ctx, `
INSERT INTO projects (code, name, customer_name, device_type, current_stage)
VALUES ($1, '智能微创内窥镜', '水木创新中心', '内窥镜', 'V0.5_feasibility')
RETURNING id
`, projCode).Scan(&projID)
if err != nil {
t.Fatalf("insert project failed: %v", err)
}
var userID int64
accountID := "acc_mgr_" + time.Now().Format("150405")
err = pool.QueryRow(ctx, `
INSERT INTO users (account_id, name, role)
VALUES ($1, '首席法规官', 'regulatory')
RETURNING id
`, accountID).Scan(&userID)
if err != nil {
t.Fatalf("insert user failed: %v", err)
}
sessID := "sess_db_" + time.Now().Format("150405")
_ = mgr.RecordUserMessage(ctx, sessID, userID, "user", "请简短汇报,结论先行,关注NMPA注册进度和排期。", "QUERY_PROGRESS", "PREEMPT", nil)
_ = mgr.RecordUserMessage(ctx, sessID, userID, "assistant", "已为您梳理完成,当前已完成法规初筛。", "", "", nil)
_, err = mgr.RecordActivity(ctx, projID, userID, "首席法规官", "decision", "选定注册检验所并锁定型检方案", nil, 1.0, true)
if err != nil {
t.Fatalf("record activity in db failed: %v", err)
}
// 验证单用户上下文读取
uCtx, err := mgr.GetUserContext(ctx, projID, userID, sessID)
if err != nil {
t.Fatalf("get user context from db failed: %v", err)
}
if len(uCtx.RecentChats) < 2 {
t.Errorf("expected at least 2 chat messages in db, got %d", len(uCtx.RecentChats))
}
if uCtx.Preference.CommunicationStyle != StyleConciseStructured {
t.Errorf("expected preference StyleConciseStructured in db, got %s", uCtx.Preference.CommunicationStyle)
}
// 验证项目级上下文读取
pCtx, err := mgr.GetProjectContext(ctx, projID)
if err != nil {
t.Fatalf("get project context from db failed: %v", err)
}
if len(pCtx.MilestoneDecisions) == 0 {
t.Errorf("expected milestone decisions in db, got 0")
}
// 验证混合上下文装配
blended, err := mgr.BuildBlendedSquadContext(ctx, projID, userID, "feasibility_reg", "regulatory")
if err != nil {
t.Fatalf("build blended context failed: %v", err)
}
if !strings.Contains(blended.PersonalizedGuidance, "结构精炼、结论先行") {
t.Errorf("blended context missing user personalized guidance: %s", blended.PersonalizedGuidance)
}
}
+137
View File
@@ -0,0 +1,137 @@
package ctxmgr
import (
"fmt"
"strings"
"time"
)
// PreferenceReflector 用户工作习惯与偏好智能总结反思器
type PreferenceReflector struct{}
func NewPreferenceReflector() *PreferenceReflector {
return &PreferenceReflector{}
}
// ReflectFromMessages 分析用户会话历史,提炼或更新用户的工作习惯画像
func (r *PreferenceReflector) ReflectFromMessages(messages []ChatMessageRecord, existing *UserPreference) *UserPreference {
if existing == nil {
existing = &UserPreference{
CommunicationStyle: StyleConciseStructured,
FocusAreas: make([]string, 0),
OutputFormatPref: "standard_report",
MemoryFacts: make(map[string]string),
UpdatedAt: time.Now(),
}
}
focusSet := make(map[string]bool)
for _, f := range existing.FocusAreas {
focusSet[f] = true
}
for _, msg := range messages {
if msg.Role != "user" {
continue
}
c := strings.ToLower(msg.Content)
// 1. 风格偏好识别
if strings.Contains(c, "简短") || strings.Contains(c, "简要") || strings.Contains(c, "说重点") ||
strings.Contains(c, "太长") || strings.Contains(c, "不要太啰嗦") || strings.Contains(c, "结论先行") {
existing.CommunicationStyle = StyleConciseStructured
} else if strings.Contains(c, "详细") || strings.Contains(c, "展开讲") || strings.Contains(c, "深度分析") || strings.Contains(c, "推演过程") {
existing.CommunicationStyle = StyleDetailedAnalytical
} else if strings.Contains(c, "高管决策") || strings.Contains(c, "决策要点") || strings.Contains(c, "高管版") || strings.Contains(c, "高管摘要") {
existing.CommunicationStyle = StyleExecutiveSummary
}
// 2. 关注维度识别
if strings.Contains(c, "法规") || strings.Contains(c, "标准") || strings.Contains(c, "合规") || strings.Contains(c, "nmpa") || strings.Contains(c, "ce") {
focusSet["reg_compliance"] = true
}
if strings.Contains(c, "成本") || strings.Contains(c, "价格") || strings.Contains(c, "预算") || strings.Contains(c, "工时") || strings.Contains(c, "报价") {
focusSet["cost_budget"] = true
}
if strings.Contains(c, "周期") || strings.Contains(c, "排期") || strings.Contains(c, "进度") || strings.Contains(c, "尽快") || strings.Contains(c, "节点") {
focusSet["timeline"] = true
}
if strings.Contains(c, "光学") || strings.Contains(c, "结构") || strings.Contains(c, "硬件") || strings.Contains(c, "参数") || strings.Contains(c, "公差") {
focusSet["technical_parameters"] = true
}
// 3. 输出格式偏好
if strings.Contains(c, "双语") || strings.Contains(c, "英文") {
existing.OutputFormatPref = "bilingual_brief"
} else if strings.Contains(c, "ppt") || strings.Contains(c, "幻灯片") || strings.Contains(c, "要点清单") {
existing.OutputFormatPref = "ppt_bullets"
}
// 4. 定制化指令捕获 (如 "记住...", "以后请...")
if strings.Contains(c, "记住") || strings.Contains(c, "以后请") || strings.Contains(c, "默认都") {
existing.CustomInstructions = msg.Content
}
}
var updatedFocus []string
for f := range focusSet {
updatedFocus = append(updatedFocus, f)
}
existing.FocusAreas = updatedFocus
existing.UpdatedAt = time.Now()
return existing
}
// GeneratePromptGuidance 将用户偏好编译为注入到智能体或参谋 Prompt 的定制化指导语
func (r *PreferenceReflector) GeneratePromptGuidance(pref *UserPreference) string {
if pref == nil {
return ""
}
var sb strings.Builder
sb.WriteString("【用户个性化工作习惯与偏好指导 (Memory Injection)】\n")
switch pref.CommunicationStyle {
case StyleConciseStructured:
sb.WriteString("• 沟通风格:该用户偏好【结构精炼、结论先行】的呈现方式,严禁冗长铺垫,优先给出摘要与下一步行动。\n")
case StyleDetailedAnalytical:
sb.WriteString("• 沟通风格:该用户偏好【详尽论证与深度分析】,请提供充分的依据、竞品横向对比与技术参数细节。\n")
case StyleExecutiveSummary:
sb.WriteString("• 沟通风格:该用户偏好【高管决策摘要】,突出商务价值、风险敞口与关键资源投入,弱化底层算法推演。\n")
}
if len(pref.FocusAreas) > 0 {
var areaNames []string
for _, a := range pref.FocusAreas {
switch a {
case "reg_compliance":
areaNames = append(areaNames, "法规标准合规与审评审批风险")
case "cost_budget":
areaNames = append(areaNames, "CDO工时核算、BOM成本与预算边界")
case "timeline":
areaNames = append(areaNames, "关键研发排期、型检周期与备料冗余")
case "technical_parameters":
areaNames = append(areaNames, "核心光学/机械/电气参数与工艺公差")
default:
areaNames = append(areaNames, a)
}
}
sb.WriteString(fmt.Sprintf("• 重点关注:该用户高度敏感于【%s】,请在产出结论时予以重点呈现并做避险预警。\n", strings.Join(areaNames, "、")))
}
if pref.OutputFormatPref != "" && pref.OutputFormatPref != "standard_report" {
switch pref.OutputFormatPref {
case "bilingual_brief":
sb.WriteString("• 格式要求:用户偏好附带中英双语核心术语与摘要。\n")
case "ppt_bullets":
sb.WriteString("• 格式要求:文末请专门附带适合制作汇报 PPT 的条目清单 (Bullet Points)。\n")
}
}
if pref.CustomInstructions != "" {
sb.WriteString(fmt.Sprintf("• 用户明确要求:%s\n", pref.CustomInstructions))
}
return sb.String()
}
+125
View File
@@ -0,0 +1,125 @@
package ctxmgr
import (
"time"
"chat-agent-system/internal/storage"
)
// CommunicationStyle 用户沟通风格
type CommunicationStyle string
const (
StyleConciseStructured CommunicationStyle = "concise_structured" // 结构精炼、结论先行、拒绝冗长
StyleDetailedAnalytical CommunicationStyle = "detailed_analytical" // 详尽论证、全面对比、深挖细节
StyleExecutiveSummary CommunicationStyle = "executive_summary" // 高管要点、商业决策导向
)
// UserPreference 用户工作习惯与产出偏好画像
type UserPreference struct {
UserID int64 `json:"user_id"`
AccountID string `json:"account_id"`
Name string `json:"name"`
Role string `json:"role"`
CommunicationStyle CommunicationStyle `json:"communication_style"`
FocusAreas []string `json:"focus_areas"` // 关注维度,如 "reg_compliance", "cost_budget", "timeline", "optical_quality"
OutputFormatPref string `json:"output_format_pref"` // 报告格式要求,如 "standard_report", "bilingual_brief", "ppt_bullets"
CustomInstructions string `json:"custom_instructions"` // 用户直接指定的定制要求
MemoryFacts map[string]string `json:"memory_facts"` // 异步蒸馏出的长期事实记忆 (如 "预算通常按千万级申报", "偏好使用迈瑞作为对标竞品")
UpdatedAt time.Time `json:"updated_at"`
}
// ActivityItem 项目活动记录 (操作、决策、上传、下载)
type ActivityItem struct {
ID int64 `json:"id"`
ProjectID int64 `json:"project_id"`
UserID int64 `json:"user_id"`
UserName string `json:"user_name"`
ActivityType string `json:"activity_type"` // decision, operation, upload, download, stage_advance, chat
Title string `json:"title"`
Detail map[string]any `json:"detail,omitempty"`
RawWeight float64 `json:"raw_weight"` // 初始权重
DecayedWeight float64 `json:"decayed_weight"`// 时间衰减后当前有效权重
IsMilestone bool `json:"is_milestone"` // 里程碑事件不衰减
CreatedAt time.Time `json:"created_at"`
}
// TimeDecayConfig 时间权重衰减配置
type TimeDecayConfig struct {
Enabled bool `json:"enabled"`
HalfLifeDays float64 `json:"half_life_days"` // 半衰期天数 (如 7.0 天)
MinWeightFloor float64 `json:"min_weight_floor"` // 权重衰减下限 (如 0.10,防止被完全抹除)
MilestoneExempt bool `json:"milestone_exempt"` // 里程碑事件是否豁免衰减 (默认 true)
}
// DefaultTimeDecayConfig 默认时间衰减配置
func DefaultTimeDecayConfig() TimeDecayConfig {
return TimeDecayConfig{
Enabled: true,
HalfLifeDays: 7.0,
MinWeightFloor: 0.15,
MilestoneExempt: true,
}
}
// SingleUserContext 单用户私有上下文
type SingleUserContext struct {
UserID int64 `json:"user_id"`
AccountID string `json:"account_id"`
Name string `json:"name"`
Role string `json:"role"`
Preference *UserPreference `json:"preference"`
SessionID string `json:"session_id"`
RecentChats []ChatMessageRecord `json:"recent_chats"`
MyActivities []ActivityItem `json:"my_activities"`
MyFiles []*storage.FileReference `json:"my_files"`
AssembledAt time.Time `json:"assembled_at"`
}
// ProjectCollectiveContext 项目级多方协同上下文
type ProjectCollectiveContext struct {
ProjectID int64 `json:"project_id"`
ProjectCode string `json:"project_code"`
ProjectName string `json:"project_name"`
CurrentStage string `json:"current_stage"`
BlackboardFacts map[string]string `json:"blackboard_facts"`
AllActivities []ActivityItem `json:"all_activities"`
MilestoneDecisions []ActivityItem `json:"milestone_decisions"`
SharedFiles []*storage.FileReference `json:"shared_files"`
ParticipatingUsers []UserSummary `json:"participating_users"`
AssembledAt time.Time `json:"assembled_at"`
}
// UserSummary 参与项目的用户摘要
type UserSummary struct {
UserID int64 `json:"user_id"`
Name string `json:"name"`
Role string `json:"role"`
}
// ChatMessageRecord 结构化会话消息
type ChatMessageRecord struct {
ID int64 `json:"id"`
SessionID string `json:"session_id"`
UserID int64 `json:"user_id"`
Role string `json:"role"`
Content string `json:"content"`
Intent string `json:"intent,omitempty"`
Branch string `json:"branch,omitempty"`
Metadata map[string]any `json:"metadata,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
// BlendedSquadContext 智能体小队混合上下文 (用于注入到 Agent Prompt 中)
type BlendedSquadContext struct {
ProjectName string `json:"project_name"`
CurrentStage string `json:"current_stage"`
TaskKey string `json:"task_key"`
AgentRole string `json:"agent_role"`
BlackboardFacts map[string]string `json:"blackboard_facts"`
KeyDecisions []string `json:"key_decisions"`
RelevantFiles []string `json:"relevant_files"`
TriggerUserPreference *UserPreference `json:"trigger_user_preference,omitempty"`
PersonalizedGuidance string `json:"personalized_guidance"` // 针对当前用户偏好生成的注入指导语
AssembledAt time.Time `json:"assembled_at"`
}