11. 项目驱动学习法
十一、项目驱动学习法¶
基于统一业务领域的双轨道实战系统设计¶
在高级后端与 AI 工程实践中,孤立地学习检索增强生成(RAG)或智能体(Agent)常导致认知碎片化。RAG 项目易停留在简单的“向量检索-生成拼装”阶段,忽视了底层的数据一致性与权限屏障;而 Agent 项目则易沉溺于概率性的工具编排,忽视了物理副作用的幂等性与长周期状态自愈。
本节引入校园业务领域(Campus Domain)下的双轨道实战系统设计。通过在统一的业务实体边界内并发部署“校园知识库问答系统”与“论坛求助治理 Agent”,深度演练“被动知识检索”与“主动状态干预”两种截然不同的工程手感,并在运行时将二者打通,实现高内聚的系统级协同。
轨道一:多租户高并发校园知识库问答系统¶
1. 核心系统实体数据模型(Go 实现)¶
轨道一要建立一个知识检索引擎,核心是做好三件事:严格的多租户隔离、版本追踪、流式可观测性。
type Document struct {
ID string `json:"id" db:"id"`
Title string `json:"title" db:"title"`
Source string `json:"source" db:"source"`
TenantID string `json:"tenant_id" db:"tenant_id"` // 多租户隔离标识
Version int `json:"version" db:"version"` // 文档版本号
IsActive bool `json:"is_active" db:"is_active"` // 软失效标识,防止旧版本被检索
RawText string `json:"raw_text" db:"raw_text"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
}
type Chunk struct {
ID string `json:"id" db:"id"`
DocumentID string `json:"document_id" db:"document_id"`
Position int `json:"position" db:"position"` // 切块物理顺序偏移
Text string `json:"text" db:"text"`
Embedding []float32 `json:"embedding" db:"embedding"` // pgvector 高维向量
}
type ChatLog struct {
ID string `json:"id" db:"id"`
SessionID string `json:"session_id" db:"session_id"`
Question string `json:"question" db:"question"`
Answer string `json:"answer" db:"answer"`
RetrievedIds []string `json:"retrieved_ids" db:"retrieved_ids"` // 关联召回的 Chunk ID 集合,支持回溯审计
P95LatencyMs int64 `json:"p95_latency_ms" db:"p95_latency_ms"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
}
2. 系统执行流与物理拓扑¶
3. 核心设计痛点与系统级边界控制¶
上传接口要不要等文档解析完才返回?不要。接口只负责接收文档元数据、写入对象存储,然后把任务丢进 RabbitMQ 就返回。真正耗时的文本抽取、OCR、清洗、语义切块和批量向量化,都交给后台 Worker 并发处理。Document 上维护 Processing、Ready、Failed 三个状态,用来隔离这段高时延 I/O,出错了也方便重试。
版本淘汰是另一个容易漏掉的点。教务处发布新版退选通知后,旧版文档不能继续被检索到,否则模型会拿着过期信息作答。做法是在 Chunk 查询阶段做硬性前置过滤:离线管线把旧版 Document.IsActive 置为 false,检索时始终带上 WHERE is_active = true AND tenant_id = :tenant_id。
引用要能对得上原文,不能靠模型自己编。 具体做法是让模型生成的文本块携带 [chunk_idx] 这样的标识,API 在 SSE 流式写回时拦截这个标识,替换成真实的 Document.Source 和片段链接,用户看到的每一句话都能点回原文。
客户端断开连接了,服务端还要不要继续算下去?不应该。Go 里用 r.Context() 监听 TCP 断连,一旦触发,信号级联传给下游连接池,尚未完成的 LLM 流式通道和检索 goroutine 立刻熔断,避免孤儿请求占着资源不撒手。
轨道二:基于状态自愈与审计治理的论坛求助治理 Agent¶
1. 核心系统实体数据模型(Go 实现)¶
轨道二要构建一个主动治理引擎:自动扫描论坛帖子、提取求助意图、建议下一步动作,并且能在长周期事务中自愈。
type HelpPost struct {
ID string `json:"id" db:"id"`
Title string `json:"title" db:"title"`
Body string `json:"body" db:"body"`
AuthorID string `json:"author_id" db:"author_id"`
IsResolved bool `json:"is_resolved" db:"is_resolved"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
}
type AgentRun struct {
ID string `json:"id" db:"id"`
PostID string `json:"post_id" db:"post_id"`
Status string `json:"status" db:"status"` // queued, running, pending_approval, resolved, failed
TraceID string `json:"trace_id" db:"trace_id"`
HopCounter int `json:"hop_counter" db:"hop_counter"` // 环路断路器计数
CurrentStep string `json:"current_step" db:"current_step"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
}
type AgentAction struct {
ID string `json:"id" db:"id"`
RunID string `json:"run_id" db:"run_id"`
Kind string `json:"kind" db:"kind"` // reply_post, send_dm, escalate_task
IdempotencyKey string `json:"idempotency_key" db:"idempotency_key"` // 强幂等键,防止动作重复触发
TargetID string `json:"target_id" db:"target_id"`
Status string `json:"status" db:"status"` // pending, executed, failed
ResponseRaw string `json:"response_raw" db:"response_raw"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
}
type MatchCandidate struct {
ID string `json:"id" db:"id"`
Kind string `json:"kind" db:"kind"` // student_helper, office_staff, resource_entity
Score float64 `json:"score" db:"score"`
Reason string `json:"reason" db:"reason"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
}
type ConversationSummary struct {
ThreadID string `json:"thread_id" db:"thread_id"`
Facts string `json:"facts" db:"facts"` // 提炼后的硬事实骨架
OpenIssues string `json:"open_issues" db:"open_issues"` // 悬而未决的待办项
LastVersion int `json:"last_version" db:"last_version"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
}
2. 系统执行流与物理拓扑¶
3. 核心设计痛点与系统级边界控制¶
扫描定时器可能重复拉起同一个帖子——网络抖动、任务重试都会导致这种情况。如果不做防护,Agent 就可能对同一个求助重复发私信、重复建任务。解决办法是在执行任何 AgentAction 之前生成一个唯一的 IdempotencyKey(一般用 PostID + Kind + TargetID 的哈希),事务内 INSERT INTO agent_actions 时一旦撞上唯一键冲突,直接放弃这次执行。
Agent 之间会不会互相触发、绕成死循环? 会的:Agent A 私信 Agent B,B 触发回帖,回帖又被 A 扫描到,链路就绕回去了。防法是给 AgentRun 染色传递 trace_id 和 hop_counter,每流转一步计数器加一,一旦超过上限(比如 6),就强制熔断,挂起任务转人工接管。
多轮求助跟进如果把全部历史原样塞给模型,指令会被稀释,运行开销也会飙升。所以每轮交互结束后跑一次增量压缩:剥掉语气词和寒暄,只留“已确立事实”和“挂起问题”两块,写进 ConversationSummary。下一轮只加载这份摘要,模型的工作记忆才能保持干净。
学生发帖里经常夹带联系方式、学号这类隐私信息。这些内容提交给外部大模型之前,必须先过一道本地过滤或轻量 PII 检测、做脱敏;Agent 生成的回帖内容在发出去之前,也要过白名单词库和安全围栏,防止说出不该说的话。
双轨道实战系统演进路线图¶
双轨实战系统绝非一次性构建的 Demo,而必须严格遵循分层迭代的演进路径,以防止开发路径过长导致项目失控崩溃。
| 迭代阶段 | 轨道一 (Campus RAG) 建设卡点 | 轨道二 (Campus Agent) 建设卡点 | 支撑系统核心指标与安全基准 |
|---|---|---|---|
| v1 阶段 (骨架贯通) | 1. 跑通同步文档上传、提取、切块。 2. 向量检索 Top-K 文本片段直接送入 LLM。 3. 实现基本的流式 HTTP SSE 接口。 |
1. 定期扫描论坛求助帖事件。 2. 大模型单步判定是否需要求助。 3. 只读调用 RAG 接口返回自动回帖建议。 |
核心目标:单次 OODA 受控循环跑通,无并发冲突,打通底座 Go 后端与 LLM 的基本连接。 |
| v2 阶段 (边界治理) | 1. 引入 RabbitMQ,将解析与向量化彻底异步化。 2. 实现 Pre-filtering 多租户与版本淘汰过滤。 3. 建立 Context 挂断信号的全链路传播回收。 |
1. 引入 IdempotencyKey 唯一索引限制,确保物理操作幂等。2. 建立 Task Checkpoint 并支持 Durable Execution。3. 引入高风险写动作的人工介入卡点。 |
核心目标:实现系统确定性硬边界,防范数据串漏、重复调用及孤儿资源泄露,确保 side effect 完全受控。 |
| v3 阶段 (性能与演进) | 1. 部署 BM25 + pgvector 混合检索与 RRF 融合。 2. 接入 Rerank 服务调优 P95 延迟。 3. 部署 Query Cache 与 Embedding Cache。 |
1. 部署 ConversationSummary 增量压缩管线。2. 建立 TraceID 跨应用追踪与 Hop 环路断路器。3. 搭建离线 Golden 评测集与线上影子评测。 |
核心目标:在高并发大流量下保障亚秒级延迟与极低 Token 消耗,提供长周期事务的自愈与全面治理。 |
按 v1 → v2 → v3 走完这条路线后,应该能对着自己的项目回答两类问题:轨道一的检索链路在哪一层可能返回过期或跨租户的数据、怎样堵住;轨道二的哪个动作是写副作用、幂等键和熔断条件分别是什么。答不上来,说明某个阶段还没有真正做完。
需要说明的是,校园知识库问答系统和论坛求助治理 Agent 是本节虚构出来的双轨道项目,作用是演示一种可迁移的设计组织方式,不是可以直接当真实经历讲的项目经历。真到面试或写简历时,请换成自己实际动手做过的项目。