feat(agent): 实现混合上下文装配引擎(Context Blender)与智能体小队沙箱对接

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
WeiCHEN
2026-09-08 10:23:28 +08:00
co-authored by Cursor
parent 73e053e8e0
commit 67e52ffd42
6 changed files with 220 additions and 24 deletions
+106
View File
@@ -0,0 +1,106 @@
package bizdev
import (
"context"
"os"
"path/filepath"
"strings"
"testing"
"chat-agent-system/internal/config"
"chat-agent-system/internal/ctxmgr"
)
func TestService_BlendedContextInjection(t *testing.T) {
tempDir, err := os.MkdirTemp("", "blended_ctx_test_*")
if err != nil {
t.Fatalf("mkdir temp failed: %v", err)
}
defer os.RemoveAll(tempDir)
cfg := &config.Config{
Env: "test",
DataDir: tempDir,
AgentBackend: "stub",
}
svc := NewService(cfg)
ctx := context.Background()
// 1. 设置用户个性化偏好
userID := int64(108)
projectID := int64(9001)
pref := &ctxmgr.UserPreference{
UserID: userID,
AccountID: "user_blended_test",
Name: "项目总监",
Role: "director",
CommunicationStyle: ctxmgr.StyleExecutiveSummary,
FocusAreas: []string{"cost_budget", "timeline"},
OutputFormatPref: "ppt_bullets",
}
_ = svc.ContextManager().UpdateUserPreference(ctx, pref)
// 2. 记录先验里程碑决策
_, _ = svc.ContextManager().RecordActivity(ctx, projectID, userID, "项目总监", "decision", "首期控制在50万预算内并优先过型检", nil, 1.0, true)
// 3. 执行任务 (使用 stub 后端进行快速逻辑验证)
res, err := svc.ExecuteTask(ctx, TaskRunRequest{
TaskKey: "intake",
ProjectName: "微型超声内窥镜系统",
CustomerName: "北京水木东方",
DeviceType: "超声内窥镜",
TargetMarket: "国内",
Backend: "stub",
ProjectID: projectID,
UserID: userID,
})
if err != nil {
t.Fatalf("execute task failed: %v", err)
}
if !res.Success {
t.Fatalf("expected task success")
}
// 4. 验证工作区内的 context/user.md 与 context/memory/ 是否注入了混合上下文
userMdPath := filepath.Join(res.WorkDir, "context", "user.md")
userMdData, err := os.ReadFile(userMdPath)
if err != nil {
t.Fatalf("read user.md failed: %v", err)
}
userMdStr := string(userMdData)
if !strings.Contains(userMdStr, "高管决策摘要") {
t.Errorf("user.md missing executive style guidance: %s", userMdStr)
}
if !strings.Contains(userMdStr, "CDO工时核算、BOM成本与预算边界") {
t.Errorf("user.md missing cost focus: %s", userMdStr)
}
// 5. 验证 context/memory/team_decisions.md 注入
teamDecPath := filepath.Join(res.WorkDir, "context", "memory", "team_decisions.md")
teamDecData, err := os.ReadFile(teamDecPath)
if err != nil {
t.Fatalf("read team_decisions.md failed: %v", err)
}
if !strings.Contains(string(teamDecData), "首期控制在50万预算内") {
t.Errorf("team_decisions.md missing milestone decision: %s", string(teamDecData))
}
// 6. 验证任务完成后自动记录了里程碑活动
pCtx, err := svc.ContextManager().GetProjectContext(ctx, projectID)
if err != nil {
t.Fatalf("get project context failed: %v", err)
}
foundDeliveryActivity := false
for _, act := range pCtx.AllActivities {
if strings.Contains(act.Title, "完成交付物") {
foundDeliveryActivity = true
break
}
}
if !foundDeliveryActivity {
t.Errorf("expected delivery activity recorded in project context")
}
}
+89 -22
View File
@@ -10,23 +10,57 @@ import (
"time"
"chat-agent-system/internal/config"
"chat-agent-system/internal/ctxmgr"
"chat-agent-system/internal/kb"
"chat-agent-system/internal/squad"
"chat-agent-system/internal/storage"
"chat-agent-system/internal/stream"
"github.com/jackc/pgx/v5/pgxpool"
)
type Service struct {
cfg *config.Config
kb *kb.Client
squad *squad.Service
cfg *config.Config
kb *kb.Client
squad *squad.Service
ctxMgr *ctxmgr.ContextManager
cas *storage.CAS
}
func NewService(cfg *config.Config) *Service {
return &Service{
cfg: cfg,
kb: kb.NewClient(cfg),
squad: squad.NewService(cfg),
if cfg == nil {
cfg = &config.Config{
DataDir: "./data/dev",
}
}
var pool *pgxpool.Pool
if cfg.ContextDbHost != "" {
poolCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
if p, err := pgxpool.New(poolCtx, cfg.ContextDSN()); err == nil {
if p.Ping(poolCtx) == nil {
pool = p
}
}
}
casStore, _ := storage.NewCAS(filepath.Join(cfg.DataDir, "storage"), pool, nil)
ctxMgr := ctxmgr.NewContextManager(pool, casStore, ctxmgr.DefaultTimeDecayConfig())
return &Service{
cfg: cfg,
kb: kb.NewClient(cfg),
squad: squad.NewService(cfg),
ctxMgr: ctxMgr,
cas: casStore,
}
}
func (s *Service) ContextManager() *ctxmgr.ContextManager {
return s.ctxMgr
}
func (s *Service) CAS() *storage.CAS {
return s.cas
}
var SupportedTasks = squad.SupportedTasks
@@ -111,22 +145,46 @@ func (s *Service) ExecuteTask(ctx context.Context, req TaskRunRequest) (*TaskRun
}
}
// 混合上下文装配 (Context Blender)
userCtxStr := req.UserContext
memBank := req.MemoryBankFiles
if memBank == nil {
memBank = make(map[string]string)
}
if s.ctxMgr != nil {
blended, err := s.ctxMgr.BuildBlendedSquadContext(ctx, req.ProjectID, req.UserID, req.TaskKey, "squad")
if err == nil && blended != nil {
if userCtxStr == "" && blended.PersonalizedGuidance != "" {
userCtxStr = blended.PersonalizedGuidance
}
if blended.PersonalizedGuidance != "" {
memBank["user_preferences.md"] = blended.PersonalizedGuidance
}
if len(blended.KeyDecisions) > 0 {
memBank["team_decisions.md"] = "# 项目团队关键决策与里程碑记录\n\n" + strings.Join(blended.KeyDecisions, "\n")
}
}
}
squadReq := squad.SquadRunRequest{
TaskKey: req.TaskKey,
ProjectName: req.ProjectName,
CustomerName: req.CustomerName,
DeviceType: req.DeviceType,
IntendedUse: req.IntendedUse,
TargetMarket: req.TargetMarket,
Fields: req.Fields,
Materials: materials,
Backend: req.Backend,
Timeout: req.Timeout,
WorkDir: req.WorkDir,
CustomSteps: req.CustomSteps,
AutoRepair: req.AutoRepair,
KnowledgeHits: hits,
PriorDocs: req.PriorDocs,
TaskKey: req.TaskKey,
ProjectName: req.ProjectName,
CustomerName: req.CustomerName,
DeviceType: req.DeviceType,
IntendedUse: req.IntendedUse,
TargetMarket: req.TargetMarket,
Fields: req.Fields,
Materials: materials,
Backend: req.Backend,
Timeout: req.Timeout,
WorkDir: req.WorkDir,
CustomSteps: req.CustomSteps,
AutoRepair: req.AutoRepair,
KnowledgeHits: hits,
PriorDocs: req.PriorDocs,
UserContext: userCtxStr,
MemoryBankFiles: memBank,
}
res, err := s.squad.ExecuteTask(ctx, squadReq)
@@ -146,6 +204,13 @@ func (s *Service) ExecuteTask(ctx context.Context, req TaskRunRequest) (*TaskRun
return nil, err
}
if res.Success && s.ctxMgr != nil && req.ProjectID > 0 {
_, _ = s.ctxMgr.RecordActivity(ctx, req.ProjectID, req.UserID, "系统智能体小队", "operation",
fmt.Sprintf("完成交付物【%s】", res.ArtifactTitle),
map[string]any{"task_key": res.TaskKey, "work_dir": res.WorkDir},
1.0, true)
}
return &TaskRunResult{
TaskKey: res.TaskKey,
ArtifactTitle: res.ArtifactTitle,
@@ -245,6 +310,8 @@ func (s *Service) executeStageInternal(ctx context.Context, req StageRunRequest)
AutoRepair: req.AutoRepair,
KnowledgeHits: req.KnowledgeHits,
PriorDocs: accumulatedPriorDocs,
ProjectID: req.ProjectID,
UserID: req.UserID,
}
res, err := s.ExecuteTask(ctx, tReq)
+8 -2
View File
@@ -48,8 +48,12 @@ type TaskRunRequest struct {
WorkDir string `json:"work_dir"`
CustomSteps []squad.StepSpec `json:"custom_steps,omitempty"`
AutoRepair bool `json:"auto_repair,omitempty"`
KnowledgeHits []squad.KnowledgeHit `json:"knowledge_hits,omitempty"`
PriorDocs []squad.PriorDocItem `json:"prior_docs,omitempty"`
KnowledgeHits []squad.KnowledgeHit `json:"knowledge_hits,omitempty"`
PriorDocs []squad.PriorDocItem `json:"prior_docs,omitempty"`
UserID int64 `json:"user_id,omitempty"`
ProjectID int64 `json:"project_id,omitempty"`
UserContext string `json:"user_context,omitempty"`
MemoryBankFiles map[string]string `json:"memory_bank_files,omitempty"`
}
type TaskRunResult struct {
@@ -84,6 +88,8 @@ type StageRunRequest struct {
AutoRepair bool `json:"auto_repair,omitempty"`
PriorDocs []squad.PriorDocItem `json:"prior_docs,omitempty"`
KnowledgeHits []squad.KnowledgeHit `json:"knowledge_hits,omitempty"`
UserID int64 `json:"user_id,omitempty"`
ProjectID int64 `json:"project_id,omitempty"`
}
// StageRunResult 成熟度阶段执行结果(双轨交付:A轨合规文档 + B轨业务参谋锦囊)
+10
View File
@@ -6,6 +6,7 @@ import (
"time"
"chat-agent-system/internal/coordinator"
"chat-agent-system/internal/ctxmgr"
"chat-agent-system/internal/intent"
"chat-agent-system/internal/squad"
"chat-agent-system/internal/stream"
@@ -38,6 +39,7 @@ type Session struct {
UserCtx *coordinator.UserProjectContext `json:"user_ctx"`
Recognizer intent.Recognizer `json:"-"`
Coordinator *coordinator.CoordinatorEngine `json:"-"`
CtxMgr *ctxmgr.ContextManager `json:"-"`
Messages []Message `json:"messages"`
CreatedAt time.Time `json:"created_at"`
}
@@ -91,6 +93,10 @@ func (s *Session) sendMessageInternal(ctx context.Context, userInput string, str
}
s.Messages = append(s.Messages, userMsg)
if s.CtxMgr != nil {
_ = s.CtxMgr.RecordUserMessage(ctx, s.ID, 1, "user", userInput, "", "", nil)
}
// 1. 意图识别 (L1 对话层,带非阻塞桌角备忘快照)
var snap *intent.ContextSnapshot
if s.UserCtx != nil {
@@ -148,6 +154,10 @@ func (s *Session) sendMessageInternal(ctx context.Context, userInput string, str
}
s.Messages = append(s.Messages, asstMsg)
if s.CtxMgr != nil {
_ = s.CtxMgr.RecordUserMessage(ctx, s.ID, 1, "assistant", coordRes.NaturalReply, string(coordRes.Intent), string(recRes.Branch), nil)
}
// 4. 流式推送参谋自然语言回复与决策锦囊
if streamer != nil {
if coordRes.NaturalReply != "" {
+3
View File
@@ -176,6 +176,9 @@ func (s *Server) getOrCreateSession(
}
sess := chat.NewSession(sessionID, userCtx, s.rec, s.engine)
if s.svc != nil {
sess.CtxMgr = s.svc.ContextManager()
}
s.sessions[sessionID] = sess
return sess
}
+4
View File
@@ -47,6 +47,8 @@ type SquadRunRequest struct {
AutoRepair bool `json:"auto_repair"`
KnowledgeHits []KnowledgeHit `json:"knowledge_hits"`
PriorDocs []PriorDocItem `json:"prior_docs"`
UserContext string `json:"user_context,omitempty"`
MemoryBankFiles map[string]string `json:"memory_bank_files,omitempty"`
StepNotify func(StepEvent) `json:"-"`
}
@@ -194,6 +196,8 @@ func (s *Service) ExecuteTask(ctx context.Context, req SquadRunRequest) (*SquadR
PriorDocs: req.PriorDocs,
WorkflowSteps: req.CustomSteps,
InitialFields: req.Fields,
UserContext: req.UserContext,
MemoryBankFiles: req.MemoryBankFiles,
}
_, err := s.engine.PrepareWorkspace(wsReq)