跳转至

11. 项目驱动学习法

十一、项目驱动学习法

先把两个半成品放到同一所校园里

只给一个 RAG 项目,练习容易停在”资料查出来,答案能流式返回”这一层。只给一个 Agent 项目,又会一头扎进工具循环和动作执行,资料底座、引用可信和版本更新都被丢在身后。贴近真实工程的练法是在同一个业务世界里同时练两种能力。

场景收在同一所校园里。第一条项目线是校园知识库问答系统,资料来自校历、教务通知、奖助学金说明、宿舍规则、报修流程、社团活动说明和学生事务 FAQ,练的是”怎样把校园资料查对”。第二条项目线是校园论坛求助治理 Agent,面对迟迟没人响应的求助帖、过长的 C2C 撮合链路和越拖越重的长周期跟进对话,练的是”当系统开始主动做事以后,边界会怎么突然变硬”。

两条线不是毫无关系的样板。学生问”宿舍空调坏了怎么报修”,知识库项目回答流程、地点和时间。论坛里有人发帖说”6 号楼空调坏了两天,报修电话打不通”,Agent 项目碰到识别、匹配、回帖、私信、跟进和人工接管。前者处理稳定资料,后者处理活的请求。前者装一套查证能力,后者把系统推向持续运行和动作治理。

同样是校园场景,查资料和推进事情是两种不同的工程手感。

先做一个校园知识库问答系统

第一条线做得朴素一点。系统目标是校园知识库问答服务。老师和学生上传校历、制度通知、办事指南和 FAQ,系统处理成可检索内容,另一个接口接收问题并返回答案。入口只留最少一组,POST /documents 负责上传,POST /chat 负责普通问答,POST /chat/stream 负责流式回答。数据库里只放最基础的几个对象,DocumentChunkChatLog 足够把第一版跑起来。

type Document struct {
    ID        string
    Title     string
    Source    string
    RawText   string
    CreatedAt time.Time
}

type Chunk struct {
    ID         string
    DocumentID string
    Position   int
    Text       string
    Embedding  []float32
}

type ChatLog struct {
    ID        string
    Question  string
    Answer    string
    CreatedAt time.Time
}

// POST /documents
// POST /chat
// POST /chat/stream

按最直接的办法,v1 的实现在同步请求里完成解析、切块、embedding 和落库。问答请求先检索 chunks,把命中的几段文本拼进 prompt,让模型组织回答。用户想边看边等时,走 SSE 把模型增量结果持续往前端推。学生问”奖学金申请截止到哪天””宿舍空调坏了怎么报修””晚归要怎么报备”,系统能给出一版可演示的回答。

flowchart LR A["上传校历 / 通知 / FAQ"] --> B["POST /documents"] B --> C["解析 / 切块 / embedding"] C --> D[("documents / chunks")] E["学生提问"] --> F["POST /chat 或 /chat/stream"] F --> G["召回 chunks"] G --> H["模型生成"] H --> I["答案 / 引用 / SSE"]
对象 现在承载什么 读到这里你该开始不安什么
Document 文档标题、来源、原始正文和上传时间 它现在还说不清哪一份才是当前有效版本
Chunk 被切开的文本、顺序和 embedding 它能被召回,却还不能证明引用一定可信
ChatLog 问题、答案和最基础的时间信息 它能留痕,但还回答不了这次到底错在检索还是生成

上传一份奖助学金通知,系统处理完就能被问。学生问”宿舍空调坏了怎么报修”,系统从宿舍办事指南里找出流程,再把答案流式推回来。问题从这里开始出现。

教务处发了一版新的退选时间表,老版本是否已退出召回范围。学生问到的是这学期的奖学金规则,系统是否会顺带把去年旧通知也召回。

文档增多后,上传链开始暴露问题。校历 PDF 还好,层级复杂的教务通知、混着表格和图片的奖助学金说明、带附件的宿舍管理细则会让 POST /documents 请求时间持续拉长。解析中途失败、embedding 超时或数据库写入中断时,这份资料是否算”已经入库”,重传该从头做还是接着做,系统说不清。

在线问答同样面临压力。学生看到的是完整回答,系统内部却在把检索结果、引用、流式连接和模型调用绑在一条链上。答案说”根据学生手册,晚归需要在 22:30 前报备”,哪一部分来自真实命中的 chunk,哪一部分是模型补的语气,无法区分。浏览器断开后,检索、模型流和 goroutine 是否一起停止,还是用户已离开,系统继续烧 token。

你先看到的现象 背后卡住的是哪类边界
上传一份大的通知 PDF 要等很久 任务状态
处理中途失败后不知道该不该重传 恢复点与幂等
新旧通知容易一起被召回 版本边界
答案看起来带了依据,但不敢完全信 引用装配
页面关了,流式链路可能还在跑 取消传播
改了切块或检索后,说不清到底变好没有 观测与评测

第一版能演示后,这些问题不会自行消失。知识库线继续向前推进时,任务化、版本、引用、取消传播和最小评测闭环迟早都要补齐。

这条项目线的重点不是”有没有做出一个问答框”,而是能否分出三个迭代台阶。v1 只要求链路闭环:上传、检索、回答和最基本流式跑通。v2 补版本、引用、异步建库、取消传播和最小评测集。v3 才轮到 Hybrid、rerank、增量索引、缓存和灰度。项目做不深,不是因为不会技术,而是每一版都同时追求所有能力,最后既没有主链,也说不清每一轮解决了哪种系统问题。

再做一个校园论坛求助治理 Agent

第二条线换一个更活的场景。校园论坛里每天都会出现求助帖:找二手教材、问宿舍报修、求活动搭子、发失物信息,或把具体困难丢上来求快速回应。响应常常来得太慢,真正有帮助的人和信息隔着一长串无效往返。等事情终于推进起来,帖子下已堆满杂乱回复。

这条项目线不再只是”把资料查对”,而是让系统做一串更主动的动作。先巡逻新帖子和求助会话,识别哪些内容真的是求助,再抽取求助对象、紧急度、是否适合公开暴露的信息。然后找候选资源、候选帮助者或对应校内流程。最后通过插件执行公开回帖、站内信撮合或创建跟进任务。事情没有在一轮里结束时,继续维护状态,把长周期对话压缩成下一轮真正需要的事实。

type HelpPost struct {
    ID         string
    Title      string
    Body       string
    Visibility string
    AuthorID   string
}

type AgentRun struct {
    ID          string
    PostID      string
    Status      string
    CurrentStep string
}

type AgentAction struct {
    ID        string
    RunID     string
    Kind      string
    TargetID  string
    Status    string
}

type MatchCandidate struct {
    ID    string
    Kind  string
    Score float64
    Reason string
}

type ConversationSummary struct {
    ThreadID   string
    Facts      string
    OpenIssues string
}

// scan_help_posts
// extract_help_signal
// search_candidates
// reply_post
// send_direct_message
// create_followup_task

这里最小的 v1 能做很具体的事。论坛里有人发帖说”6 号楼空调坏了两天,电话一直打不通”,系统先识别为求助,再从校园知识库里查到宿舍报修流程,决定先公开回一条能帮更多人复用的信息。帖子是”谁有离散数学教材可以转一下,明天急用”,更可能走候选匹配和站内信撮合。帖子里带了敏感信息或已有人接手,切到另一种动作节奏。

flowchart LR A["新帖子 / 新求助会话"] --> B["scan_help_posts"] B --> C["extract_help_signal"] C --> D["search_candidates / 查询校园知识库"] D --> E["动作计划"] E --> F["reply_post"] E --> G["send_direct_message"] E --> H["create_followup_task"] F --> I["AgentRun / ConversationSummary"] G --> I H --> I I --> J["人工接管或继续推进"]
对象或动作 现在在做什么 读到这里你该开始不安什么
HelpPost 保存求助帖和最初的上下文 它还没有天然区分公开信息和不该公开的信息
AgentRun 记录这次自动治理走到了哪一步 它一旦中断,谁来接着跑、接着看,还没说清
AgentAction 记录回帖、私信、建任务这些动作 动作做了,但是否幂等、是否可追责,还不够稳
MatchCandidate 存候选资源或候选帮助者 为什么匹配到这个人或这个资源,解释仍然很薄
ConversationSummary 压缩长对话里的关键事实 压缩之后保住的是业务事实,还是只省了 token,还得再看

这条链比知识库项目更像”系统会做事了”。帖子一进来,系统能识别、匹配、回帖、私信,必要时还能丢跟进任务给值班同学。拿去演示时,冲击力很容易超过单纯问答。

问题也更快出现。同一条求助帖被巡逻任务扫到两次,系统是否会连续发两封站内信。只该私下提醒的内容,如果被公开回帖说出去,责任归模型、插件还是边界没立住。

论坛求助和纯问答最大的区别在此。系统一旦开始主动回帖、发私信、建跟进任务,问题不再是”答得对不对”。它变成:动作有没有重复触发,公开和私密边界有没有分清,人工接手后 Agent 会不会继续误跑,长对话压缩以后保住了哪些事实,这次撮合有没有真的帮到人。

求助不是一轮结束的。今天公开回了流程,明天有人私信补充情况,后天又有人接手。每一轮都把全量对话重新塞回模型,成本和噪声一起上涨,真正关键的业务事实越来越散。语义压缩不再只是”为了省 token”,而是在问:哪些事实必须保住,哪些寒暄和重复说明该折叠掉。

你先看到的现象 背后卡住的是哪类边界
同一条求助帖被重复识别、重复联系 去重与幂等
公开回帖说过了头,暴露了不该公开的信息 公开/私密边界
私信已经发出去了,却说不清是谁触发的 审计与 trace
建了跟进任务,但后续没有真正回收结果 结果闭环
对话越拖越长,模型越来越贵也越来越乱 长周期状态与语义压缩
人工接手以后,Agent 还可能继续按旧状态推进 人工接管与运行状态

这些约束不会只靠调 prompt 解决。Agent 线继续向前推进时,迟早要回答动作边界、插件幂等、私信与回帖的风险分层、人工接管、状态恢复、长对话压缩和结果评估。难点不是”系统会不会说”,而是”系统开始做事以后,谁还在掌控它”。

这条线也适合按迭代台阶练。v1 只做识别、查询知识库和建议动作,不直接触发高风险写操作。v2 引入回帖、私信、建跟进任务,同时补幂等键、审计日志和人工接管。v3 继续做长周期状态压缩、checkpoint、失败恢复和多角色协作。第一版不会被 Agent 全栈复杂度淹没,同时能稳定看到”为什么一旦系统开始主动做事,治理成本会突然陡增”。

让两条项目线一起往前长

两个半成品放在同一个校园里练,最大的好处不是题材更统一,而是两种复杂度根本不是一回事。校园知识库项目在处理文档、版本、引用、取消和评测。论坛 Agent 项目在处理动作、状态、审计、人工接管和长周期对话。前者学会把资料查对,后者学会把事情推进。

两条线不是断开的。论坛 Agent 会自然地调用知识库能力,因为很多求助先需要一个靠谱的流程解释,再决定后面要不要回帖、私信或建任务。把资料查对不等于事情能推进。把事情推进起来,也不等于资料底座已经足够稳。两条线互相牵扯,放在一起练更像真实系统。

可能先受不了知识库里旧通知和新通知混着召回,也可能先受不了论坛 Agent 给同一帖子重复发私信。哪边先动手不重要。项目不再被当成”跑通一次就结束”的 demo。反复回到同一个校园场景里,问自己:这一轮补掉了哪一层边界,系统和上一轮相比到底哪里不一样了。

知识库项目有了清楚的版本和引用,论坛 Agent 调到这层能力时动作会更稳一些。论坛 Agent 的回帖、私信、跟进和人工接管能被解释、被追责、被恢复后,知识库项目”只回答问题”的边界也会显得更清楚一些。两个项目一边并列,一边互相抬高难度。

项目驱动学习不再只是”做一个能跑的东西”。它在同一个业务世界里,同时练会查资料的系统和会推进事情的系统。再在一轮轮迭代里,把它们从半成品慢慢做得像系统。