Context Builder 模式
独立的上下文组装模块,负责给定 session_id + turn_id + current_user_message,产出本轮 LLM 调用所需的完整上下文包。核心原则:不要把上下文拼接逻辑散落在各处。
#type / concept
#status / evergreen
#tech / ai
#tech / architecture
[!info] related notes
- 所属 MOC: Context Engineering MOC
- 核心概念: Context Engineering
- 协议层: Context Contract 协议
- 状态管理: 问诊状态 Schema
- 预算控制: 上下文窗口预算
- 应用: 会话与消息流设计
Context Builder 模式
Context Builder 是一个独立的上下文组装模块。它的职责是:给定 session_id + turn_id + current_user_message,产出本轮 LLM 调用所需的完整上下文包。核心原则是不要把上下文拼接逻辑散落在 chat_handler、consultation_graph、build_messages、前端 SSE 处理里。
为什么需要独立模块
没有 Context Builder 时,上下文拼接逻辑通常散落在:
- Go handler 里加载 messages
- Python 端 build_messages 函数
- 前端 SSE 处理里的状态同步
- prompt 模板里的隐式假设
这导致:
- 改一处影响多处 — 比如想调整消息过滤逻辑,要改 Go 和 Python 两个地方
- 测试困难 — 无法单独测试”给定这些输入,上下文包是否正确”
- 跨服务边界模糊 — Python 端直接查业务数据库,职责混乱
- 上下文不一致 — 不同轮次、不同代码路径组装出的上下文格式不同
职责边界
┌─────────────────────────────────────────────┐
│ Go 后端(业务主服务) │
│ │
│ - 读取数据库(session, messages, profile) │
│ - 过滤和筛选消息 │
│ - 构建结构化问诊状态 │
│ - 加载知识库检索结果 │
│ - 控制 token 预算 │
│ - 组装 Context Bundle │
│ │
│ → ContextBuilder 在这里 │
└──────────────────┬──────────────────────────┘
│ Context Contract(JSON)
▼
┌─────────────────────────────────────────────┐
│ Python AI Service(Agent 编排服务) │
│ │
│ - 接收 Context Bundle │
│ - Agent 推理和 LangGraph 流程 │
│ - 生成回复(流式) │
│ - 提取结构化信息 │
│ - 决定下一步动作 │
│ │
│ → 不负责查业务数据库 │
└─────────────────────────────────────────────┘
关键决策:Go 更接近 session、messages、user profile、extracted_info、database、auth、turn_id、message status,所以 ContextBuilder 应该在 Go 端。Python 不应该到处查业务库,否则边界会乱。
数据加载步骤
第一步:加载原始数据
加载以下数据源:
| 数据源 | 说明 |
|---|---|
| consultation_session | 会话元数据 |
| messages | 历史消息 |
| extracted_info | 已提取的结构化信息 |
| user_profile | 用户画像 |
| conversation_summary | 对话摘要(如果有) |
| agent_state | Agent 流程状态 |
第二步:消息过滤
不要简单 SELECT * FROM messages 然后全塞给模型。 必须过滤:
| 过滤规则 | 原因 |
|---|---|
| 只包含 completed 的用户消息 | 排除 pending 状态的未完成消息 |
| 只包含 completed 的 assistant 消息 | 排除 failed / canceled / partial 消息 |
| 排除当前 turn 的消息 | 避免重复(当前消息单独传) |
| 排除纯 UI 事件 | 如 phase_changed、typing 等非内容事件 |
| tool call 结果只保留摘要 | 完整结果可能过长 |
SSE 场景特殊注意:assistant 回复是流式生成的,如果中途断流,数据库里可能有半截消息。半截消息不能作为下一轮可靠上下文。
func filterMessages(messages []Message, currentTurnID string) []Message {
var filtered []Message
for _, msg := range messages {
// 排除当前 turn
if msg.TurnID == currentTurnID {
continue
}
// 只保留 completed 状态
if msg.Status != "completed" {
continue
}
// 排除纯 UI 事件
if msg.Type == "ui_event" {
continue
}
filtered = append(filtered, msg)
}
return filtered
}
第三步:构建结构化状态
不要让模型每轮都靠自然语言记忆。详见 问诊状态 Schema。
第四步:上下文窗口控制
不能永远把所有历史都塞进去。详见 上下文窗口预算。
概念结构
type ConsultationContextBundle struct {
SessionID string `json:"session_id"`
TurnID string `json:"turn_id"`
CurrentUserMessage string `json:"current_user_message"`
RecentMessages []ChatMessage `json:"recent_messages"`
ConversationSummary string `json:"conversation_summary,omitempty"`
ConsultationState ConsultationState `json:"consultation_state"`
UserProfile *UserProfile `json:"user_profile,omitempty"`
RetrievedKnowledge []KnowledgeChunk `json:"retrieved_knowledge"`
ContextMeta ContextMeta `json:"context_meta"`
}
type ContextMeta struct {
TokenBudget int `json:"token_budget"`
IncludedMessageCount int `json:"included_message_count"`
SummaryVersion int `json:"summary_version,omitempty"`
ContextBuildVersion string `json:"context_build_version"`
}
组装流程伪代码
func (b *ContextBuilder) Build(ctx context.Context, sessionID, turnID, userMessage string) (*ConsultationContextBundle, error) {
// 1. 加载会话和消息
session, _ := b.repo.GetSession(ctx, sessionID)
messages, _ := b.repo.GetMessages(ctx, sessionID)
// 2. 过滤消息
filtered := filterMessages(messages, turnID)
// 3. 分离 recent 和 summary
recent, summary := b.splitRecentAndSummary(filtered)
// 4. 加载结构化状态
state, _ := b.repo.GetConsultationState(ctx, sessionID)
// 5. 加载用户画像
profile, _ := b.repo.GetUserProfile(ctx, session.UserID)
// 6. 检索知识库(可选,按需)
knowledge := b.retrieveKnowledge(ctx, userMessage, state)
// 7. 控制 token 预算
recent = b.trimToFitTokenBudget(recent, summary, state, knowledge)
// 8. 组装 bundle
return &ConsultationContextBundle{
SessionID: sessionID,
TurnID: turnID,
CurrentUserMessage: userMessage,
RecentMessages: recent,
ConversationSummary: summary,
ConsultationState: state,
UserProfile: profile,
RetrievedKnowledge: knowledge,
ContextMeta: ContextMeta{
TokenBudget: b.tokenBudget,
IncludedMessageCount: len(recent),
ContextBuildVersion: "1.0.0",
},
}, nil
}
测试策略
Context Builder 应该有独立的单元测试:
| 测试场景 | 验证点 |
|---|---|
| 第二轮对话 | 历史消息正确包含第一轮 |
| 断流消息 | failed/partial 的 assistant 消息不进入上下文 |
| 当前 turn 排除 | 当前 turn 的消息不重复出现 |
| 长对话 | 超过阈值时触发 summary,只保留最近 N 轮 |
| 空会话 | 第一轮没有历史消息时不报错 |
| token 超限 | 自动裁剪到预算内 |
在项目中的位置
apps/api
internal/
ai_context/
builder.go ← ContextBuilder 主逻辑
message_filter.go ← 消息过滤规则
token_budget.go ← token 预算控制
consultation_state.go ← 状态加载和转换