跳转至

11. 项目驱动学习法

十一、项目驱动学习法

基于统一业务领域的双轨道实战系统设计

在高级后端与 AI 工程实践中,孤立地学习检索增强生成(RAG)或智能体(Agent)常导致认知碎片化。RAG 项目易停留在简单的“向量检索-生成拼装”阶段,忽视了底层的数据一致性与权限屏障;而 Agent 项目则易沉溺于概率性的工具编排,忽视了物理副作用的幂等性与长周期状态自愈。

本节引入校园业务领域(Campus Domain)下的双轨道实战系统设计。通过在统一的业务实体边界内并发部署“校园知识库问答系统”与“论坛求助治理 Agent”,深度演练“被动知识检索”与“主动状态干预”两种截然不同的工程手感,并在运行时将二者打通,实现高内聚的系统级协同。

flowchart TD subgraph RagTrack["轨道一:被动知识检索 (Campus RAG)"] A1["文档解析清洗"] --> A2["向量化与 pgvector 存储"] A2 --> A3["在线检索与多路融合"] A3 --> A4["流式生成与引用装配"] end subgraph AgentTrack["轨道二:主动状态干预 (Campus Agent)"] B1["事件扫描与信号提取"] --> B2["OODA 决策与行动规划"] B2 --> B3["校园 RAG 检索 (调用轨道一)"] B3 --> B4["物理副作用执行 (回帖/私信/任务)"] B4 --> B5["长周期状态自愈与语义压缩"] end A4 -. "提供精准依据" .-> B3

轨道一:多租户高并发校园知识库问答系统

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. 系统执行流与物理拓扑

flowchart TD A["离线上传: POST /documents"] --> B["写入临时 S3 并生成异步解析任务"] B --> C["Worker 集群: 解析/智能切块/Embedding"] C --> D[("存储中台: pgvector (带 Tenant_ID 与 IsActive 索引)")] E["在线提问: POST /chat/stream"] --> F["网关层: 鉴权与租户范围绑定"] F --> G["检索层: Pre-filtering 元数据过滤 + 多路召回"] G --> H["Rerank 二次重排 (Top-5 块)"] H --> I["模型调度: 绑定 Context 优雅取消流式返回 (SSE)"] I --> J["写入 ChatLog 审计库"]

3. 核心设计痛点与系统级边界控制

上传接口要不要等文档解析完才返回?不要。接口只负责接收文档元数据、写入对象存储,然后把任务丢进 RabbitMQ 就返回。真正耗时的文本抽取、OCR、清洗、语义切块和批量向量化,都交给后台 Worker 并发处理。Document 上维护 ProcessingReadyFailed 三个状态,用来隔离这段高时延 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. 系统执行流与物理拓扑

flowchart TD A["论坛扫描任务 / MQ 事件触发"] --> B["创建 AgentRun 实例 (分配 TraceID 与 HopCounter)"] B --> C["大模型提取求助信号与隐私分类检测"] C --> D{"判定动作风险等级"} D -->|只读低风险:查知识库| E["调用轨道一 RAG 获取流程说明"] D -->|高风险写副作用:发私信或创建跟进| F["校验 Action IdempotencyKey"] F --> G["写入 Checkpoint 并挂起 (Interrupt)"] G --> H["发送人工审批通知"] H -->|审批通过| I["反序列化恢复执行 (Resume)"] I --> J["触发物理 Side Effect"] J --> K["增量压缩对话事实至 ConversationSummary"]

3. 核心设计痛点与系统级边界控制

扫描定时器可能重复拉起同一个帖子——网络抖动、任务重试都会导致这种情况。如果不做防护,Agent 就可能对同一个求助重复发私信、重复建任务。解决办法是在执行任何 AgentAction 之前生成一个唯一的 IdempotencyKey(一般用 PostID + Kind + TargetID 的哈希),事务内 INSERT INTO agent_actions 时一旦撞上唯一键冲突,直接放弃这次执行。

Agent 之间会不会互相触发、绕成死循环? 会的:Agent A 私信 Agent B,B 触发回帖,回帖又被 A 扫描到,链路就绕回去了。防法是给 AgentRun 染色传递 trace_idhop_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 是本节虚构出来的双轨道项目,作用是演示一种可迁移的设计组织方式,不是可以直接当真实经历讲的项目经历。真到面试或写简历时,请换成自己实际动手做过的项目。