本页目录

综合项目:团队知识助手

按四个可验收版本串起前端、后端、知识库和业务工具,形成可展示的作品。

L2 · 能交付约 18 分钟阅读含示例、练习与验收

建议先读:需求拆解与方案设计

本页内容

项目目标:团队知识与任务助手#

这个综合项目把手册中的能力连接成一条产品交付链:用户登录,上传有权限的资料,查看处理状态,提出问题,核对引用;在需要时,让助手生成任务草稿,经确认后执行。目标是 L2 的全栈交付,同时让前端交互达到你自己的专业标准。

这是一份实施与验收指南,不是已实现的生产系统源码。各章的教学示例帮助你理解局部机制;实际项目需要统一目录、接口、数据库迁移、身份体系与运行配置。不要直接拼接内存用户、离线模型和模拟权限代码就开放给真实用户。当前实训以本机或本地容器完成开发、运行和验收,不要求申请域名、购买云服务器或公开发布网站。公网部署属于以后按需选择的扩展,不是进入下一阶段的硬门槛。

第一版的范围与技术选择#

前端使用熟悉的 Vue 3 和 TypeScript;后端使用 Node.js 与一个熟悉或愿意深入的 HTTP 框架;数据先用 PostgreSQL;文档先支持 UTF-8 文本或 Markdown;模型接入通过独立适配器。选择一个框架就够,先理解路由、校验、错误处理和测试,不同时学习多个后端框架。

前期不要求微服务、多 Agent、复杂工作流平台或独立向量数据库。把数据库、模型与存储放在清晰接口后面,项目需要时可以替换。公开部署前必须完成认证、资源权限、输入限制、密钥管理和备份验证。

统一核心领域对象#

对象 关键字段 为什么需要
User id、身份来源、状态 识别请求主体
Document id、ownerId、当前版本、访问策略 管理资料生命周期与权限
DocumentVersion versionId、内容哈希、处理状态 区分内容更新与索引发布
Chunk id、versionId、文本、位置、索引版本 检索与引用定位
Conversation id、ownerId、标题 对话归属
Message id、role、正文、引用、完成状态 持久保存可解释的会话
Attempt id、messageId、阶段、耗时、错误类别 区分生成、重试和失败
Operation id、actorId、参数、幂等键、状态 控制有副作用的动作

多租户场景增加 tenantId,并确保查询、唯一约束和后台任务都包含租户范围。记录 ownerId 只是开始,真正的隔离发生在查询条件、资源检查与工具执行时。任何后端接受的 userId 都要说明来自哪里。

接口契约草案#

下面是设计起点,具体字段要与实际实现一起维护。表中“流式”指应用自己的稳定事件协议,后端负责转换供应商响应。

接口 用途 验收重点
GET /api/me 获取当前登录主体 不返回密码与凭据
POST /api/documents 上传与登记文档 大小、类型、归属、重复提交
GET /api/documents/:id 查询状态和元信息 资源权限与不存在处理
DELETE /api/documents/:id 撤销资料并安排清理 删除后立即不可检索
POST /api/conversations 新建会话 ownerId 来自服务端身份
POST /api/conversations/:id/messages 提问并返回流式事件 权限、取消、完成标志
POST /api/operations/:id/confirm 确认执行草稿 重新校验、幂等、审计
GET /api/operations/:id 查询执行结果 响应丢失后的恢复

每个接口记录成功码、错误码、请求上限和超时。异步文档解析返回已接收状态,不能等解析完成才释放长请求。任务一旦有稳定 ID,页面刷新就可以查询状态,而不是依赖一直活着的前端连接。

版本一:普通全栈文档管理#

实现登录、文档上传、列表、详情、删除和数据库迁移。先不接模型,目的是验证你能维护身份、文件、元数据和后台状态之间的关系。准备两个用户与两份文档,确保互相越权访问失败。

验收包括:非法文件被拒绝;重复上传行为符合约定;数据库失败时不遗留无法追踪的文件;文档删除后访问立即撤销;后台处理失败可重试;重启后状态不丢失。第一版就提供可重复的本机或本地容器启动步骤、启动日志与基本健康检查。先在受控本地环境验证完整流程;只有未来明确需要公网访问时,再补充对应 HTTPS 与入口网络验收。

版本二:可恢复的 AI 对话#

加入模型适配器、会话持久化、流式渲染、取消、结构校验与费用记录。先使用离线模拟器验证界面与控制流,再配置真实模型服务。模拟器应该可以主动制造超时、错误事件和拆包。

验收包括:模型密钥只在后端;两个连续请求不会串线;网络中断显示不完整状态;取消后不会继续更新页面;错误不泄露原始凭据;用户不能读取其他人的会话。此时开始建立小规模固定评测集,后续功能在同一基线上比较。

版本三:有依据的知识库回答#

增加解析、分块、索引、权限过滤、检索、上下文组装和引用校验。先用能人工检查的小资料集,记录每个问题应命中的文档片段。对找不到答案的问题,允许系统明确说明依据不足。

验收时分开看检索与生成:正确片段是否进候选集?最终回答是否得到片段支持?引用是否指向正确版本?更新资料后旧索引是否撤销?删除或修改权限后缓存是否仍泄露内容?用固定问题集报告结果,不依赖现场挑选顺利的示例。

版本四:受控的业务操作#

加入只读查询工具和创建任务工具。模型生成操作草稿,由用户确认关键字段;服务端重新鉴权、校验并执行,使用幂等键和结果记录。限制调用步数、总耗时、允许工具与单次预算。

验收包括:模型不能把自己的建议变成授权;重复确认只产生一次任务;任务已成功但响应丢失时可查回结果;工具失败不会被展示成成功;日志可以追踪谁确认、执行了什么以及结果如何。多 Agent 暂不作为必需能力,先把一个流程的失败恢复做清楚。

一个可运行的验收记录检查器#

下面的工具不会测试你的应用;它检查你是否真的为关键场景登记了证据,防止演示后遗漏边界。保存为 check-evidence.mjs,运行 node check-evidence.mjs。样例记录是明确标注的教学数据,实际项目应替换为真实测试记录。

js
// 教学记录:这里只演示报告校验,并不代表真实项目已通过。
const required = ['正常问答', '无权限访问', '中途断流', '重复操作', '资料删除'];
const evidence = [
  { scenario: '正常问答', status: 'passed', artifact: '本地测试报告#1' },
  { scenario: '无权限访问', status: 'passed', artifact: '双用户集成测试#2' },
  { scenario: '中途断流', status: 'blocked', artifact: '尚未搭建故障模拟器' },
  { scenario: '重复操作', status: 'passed', artifact: '幂等测试#3' },
];
const byName = new Map();
for (const row of evidence) {
  if (byName.has(row.scenario)) throw new Error(`重复场景:${row.scenario}`);
  byName.set(row.scenario, row);
}
const unresolved = required.filter(name => {
  const row = byName.get(name);
  // passed 还必须附带可定位证据;一句“看过没问题”不够。
  return !row || row.status !== 'passed' || !row.artifact?.trim();
});
if (unresolved.length) {
  console.log('未完成验收:' + unresolved.join('、'));
  process.exitCode = 1;
} else {
  console.log('所有必需场景都有通过记录,请继续核对证据真实性。');
}

预期输出“未完成验收:中途断流、资料删除”,退出码为 1。这是故意设计的失败,说明报告能揭示缺口。把未完成记录补成真实验证结果之后,检查器才应该通过。不要为了让脚本变绿而随手把 status 改成 passed。

作品交付应包含什么#

准备可启动的源码、无密钥配置样例、数据库迁移、初始化数据说明、关键测试、部署与回滚步骤,以及一份清晰 README。演示视频或截图可以帮助理解交互,但不能代替启动说明。记录已知限制:支持哪些文件、允许多少并发、哪些工具必须人工确认、模型费用如何控制。

说明自己的贡献时,重点讲需求取舍、系统边界和故障处理。即使用 AI 写了很多代码,你仍应该能从一个用户问题追踪到数据库和模型调用,并解释验证证据。这比堆砌使用过的工具名更能说明你的工程能力。

先定义一条能够走到底的用户任务#

综合项目不应从“把目录全部搭好”开始,而应从一个真实任务开始:团队成员上传一份制度文档,等待它可检索,提问并打开引用,之后更新文档,再确认回答使用了新版本。这个任务同时穿过前端状态、后台处理、数据库、检索和模型边界,足以暴露单个示例看不出的组合问题。

第一条任务链可以只支持 UTF-8 文本,限制文件大小和资料数量,用户体系采用项目中明确实现的登录方式。减少文件类型是为了先验证生命周期,不是永久产品限制。每增加一种格式,都要补充解析失败、大小限制、位置引用和安全检查,而不是只在上传按钮的 accept 中多加后缀。

先准备固定资料和问题。例如一份差旅报销规则、一个有明确答案的问题、一个资料没有涉及的问题,以及一个其他用户无权读取的资料。预先写出允许引用的文档版本和关键结论。演示时按照这组固定任务执行,可以避免只挑顺利问题,也让后续修改有稳定比较基线。

需求说明还应写出失败时用户能做什么。解析失败能否重试,旧版是否继续可用,生成中断是否保留已有内容,引用文档被删除后怎样提示,任务草稿未确认是否会过期。这些不是最后补的异常文案,而是会影响数据库状态和接口设计的核心规则。

将页面字段、接口字段和数据约束连起来#

从上传列表的一行开始建字段链。页面显示文件名、当前处理状态、更新时间和可否提问;接口必须说明这些值来自哪个版本;数据库需要区分正在处理的版本与已发布版本。若只有 Document.status 一个字段,文档更新时很容易把旧版可用状态和新版处理中状态混在一起。

DocumentVersion 可以保存内容哈希、解析状态、失败类别和处理任务标识,Document 保存当前发布版本。新版本失败时不修改已发布指针,旧版继续承担查询;新版本全部准备完成后,再在受控事务或一致性流程中切换发布指针。这个设计是可选择的产品语义,第一版应在范围说明里明确,不能由前后端各自猜测。

引用需要保存文档、版本、片段和位置,而不仅是一条标题字符串。用户打开引用时,服务端重新检查资源权限并定位到对应内容。若原版本已经撤销,应显示真实状态,不能静默打开新版相似文本冒充当时证据。为了支持审计而保留历史内容,也必须遵守权限与保留规则。

删除首先撤销访问,再安排物理清理。数据库文档标记为不可用后,检索过滤、缓存和下载接口都应立即尊重这个状态;异步删除向量或文件可以稍后完成。只把上传列表的一行隐藏,却仍允许向量检索命中,是常见的跨层缺口。

会话与消息同样需要版本和状态。一次提问可以产生多次上游尝试,但应有稳定业务消息标识。部分输出、完成答案、失败原因和引用分别保存,避免重试把历史记录覆盖成无法解释的最终文本。费用账目记录实际尝试,用户界面记录一次任务,两层通过标识关联。

一个完整的离线版本发布实验#

以下程序演示“新版失败时旧版继续可用、越权拒绝、删除立即撤销检索”三个规则。保存为 capstone-flow.mjs,使用 Node.js 22 执行。它只操作内存中的教学文本,没有 HTTP 服务、数据库、向量模型或真实生成能力,不能视为综合项目成品。

javascript
// capstone-flow.mjs
import assert from "node:assert/strict";
const documents = new Map();
let versionCounter = 0;
function accessible(userId, documentId) {
  const document = documents.get(documentId);
  if (!document || document.deleted || document.ownerId !== userId) {
    throw new Error("DOCUMENT_NOT_AVAILABLE");
  }
  return document;
}
function upload(userId, documentId, text) {
  if (typeof text !== "string" || !text.trim()) throw new Error("EMPTY_DOCUMENT");
  let document = documents.get(documentId);
  if (!document) {
    document = { id: documentId, ownerId: userId, deleted: false, versions: [], published: null };
    documents.set(documentId, document);
  } else {
    document = accessible(userId, documentId);
  }
  const version = { id: "v" + ++versionCounter, text, status: "queued", chunks: [] };
  document.versions.push(version);
  return version.id;
}
function processVersion(userId, documentId, versionId, fail = false) {
  const document = accessible(userId, documentId);
  const version = document.versions.find(item => item.id === versionId);
  if (!version || version.status !== "queued") throw new Error("INVALID_JOB_STATE");
  version.status = "processing";
  if (fail) {
    version.status = "failed";
    return;
  }
  version.chunks = version.text.split("\n").filter(Boolean)
    .map((text, index) => ({ id: version.id + ":" + index, text }));
  version.status = "ready";
  document.published = version.id;
}
function retrieve(userId, documentId, query) {
  const document = accessible(userId, documentId);
  const version = document.versions.find(item => item.id === document.published);
  if (!version) return [];
  // 教学中用包含匹配替代检索引擎,不代表语义检索质量
  return version.chunks.filter(chunk => chunk.text.includes(query))
    .map(chunk => ({ documentId, versionId: version.id, ...chunk }));
}
const first = upload("u1", "d1", "报销必须提供发票。\n提交后由负责人审核。");
processVersion("u1", "d1", first);
assert.equal(retrieve("u1", "d1", "发票")[0].versionId, first);
const second = upload("u1", "d1", "新版制度尚未处理完成。");
processVersion("u1", "d1", second, true);
assert.equal(retrieve("u1", "d1", "发票")[0].versionId, first);
assert.throws(() => retrieve("u2", "d1", "发票"), /DOCUMENT_NOT_AVAILABLE/);
accessible("u1", "d1").deleted = true;
assert.throws(() => retrieve("u1", "d1", "发票"), /DOCUMENT_NOT_AVAILABLE/);
console.log("旧版保留、权限隔离、删除撤销三项规则通过");

执行 node capstone-flow.mjs,预期输出三项规则通过。将失败分支改成清空 document.published,旧版保留断言应失败;删除 accessible 中的 ownerId 条件,越权断言应失败。这些实验让你确认验收确实能发现目标缺陷。

程序没有模拟后台并发。真实任务要验证任务属于文档和版本,防止同一任务重复处理;发布指针更新要考虑较旧任务晚于新任务完成的情况。可以在提交发布时比较预期版本或任务代次,拒绝旧任务反向覆盖。内存函数同步执行不会暴露这一竞争,需要在后续数据库实现里增加对应集成测试。

将离线模型逐步替换成真实边界#

第一步用可控制的模拟器生成事件,测试前端的流式追加、终止、错误和取消;第二步用真实后端接口替换前端模拟传输,但模型仍可离线;第三步再按自己的账号与预算接入真实模型。每一步只增加一类不确定性,排障更容易。无需为了进入第二步先公开发布网站。

模型适配器返回供应商无关的内部结果,并保留业务需要的输出项目、用量和请求编号。文本问答与工具调用对输出的要求不同,不能让只提取字符串的封装吞掉 function_call 项。结构化输出仍要验证字段和业务含义,不能因为解析成功就直接写入数据库。

检索也可以从确定的关键词匹配或小型本地索引开始,但要明确当前实现能力。之后接入 embedding 与重排时,用同一资料集比较候选命中和答案证据。不要把 UI 出现引用卡片当作 RAG 正确;卡片可能指向不支持答案的段落,必须检查实际证据关系。

任务创建工具应先只生成草稿,显示标题、描述、对象和影响范围,再由可信确认请求触发服务端执行。确认后重新检查权限与对象状态,重复确认使用同一操作标识。模型输出“创建成功”不能决定 UI 状态,真实任务记录和执行结果才是依据。

在本地环境完成阶段验收#

本机验收可以使用本地前端、Node 服务和本地容器数据库。README 应说明运行时版本、依赖安装、环境变量样例、迁移、启动端口和测试账号建立方式。样例配置不包含真实密钥;真实模型未接入时应清晰显示模拟模式,避免把固定答案误当实际生成。

每阶段准备正常与失败的成对案例。上传成功对应格式错误和数据库写入失败;回答完成对应断流和取消;引用可读对应无权限和已删除版本;任务创建成功对应重复确认和响应丢失。测试不需要一开始铺满所有组合,但必须覆盖当前阶段真正承担的边界。

重启恢复应在本地真实验证:创建任务或上传资料后停止后端,再启动并查询状态,确认持久数据仍可解释。备份恢复可针对隔离的本地数据库,恢复到新目标后核对记录和应用读取,不覆盖自己的工作数据库。这些能力比是否拥有公网链接更能证明已经掌握全栈交付。

作品说明里分开列出已实现、模拟、未实现和未验证。例如“已实现文档归属检查,模型使用离线 fixture,语义检索尚未接入,公网入口未配置”。这种说明让评审知道演示实际证明什么,也帮助你安排下一步学习。未公开部署不等于作品无效,夸大模拟能力才会破坏可信度。

综合练习:把一个需求追踪到验收记录#

需求是“删除资料后,后续提问不能引用它;已打开的历史回答仍显示原引用,但点击时明确告知不可访问”。请分别写出页面行为、接口响应、数据变化和测试。不要把“删除按钮可点击”当作完整验收。

完整参考交付说明

页面行为:删除完成后从可选知识范围移除资料,历史回答保留引用标题与原版本标识;打开引用得到不可访问状态时显示清楚提示,不伪装成空文档。用户再次发送问题时,后端不会检索已删除资料,前端缓存也不能单独决定授权。

接口合同:删除接口先在当前身份范围内标记撤销;引用读取与检索都通过统一权限条件;资源不存在和无权限可按产品安全合同统一返回。接口名称沿用前面的草案,具体字段与实现同步维护,不新增一个只有文档里存在的假服务。

数据变化:保留必要的消息与引用关联,文档访问状态立即撤销,原文件和索引进入清理任务。清理失败可以重试,但不能让资料恢复可见;是否保留历史正文由实际保留规则决定。

验收:用户甲删除自己的资料后检索不再返回该版本;用户乙从始至终无法读取;历史引用仍能显示上下文,但打开结果不可访问;重启服务后撤销状态仍在。再注入索引物理清理失败,验证访问过滤仍然有效。每条记录附实际命令、结果和证据位置,未执行项保留为未完成。

练习与参考答案#

为自己的第一个版本写一页范围说明:必须实现五项、暂不实现三项、验收至少八项,再写出进入下一版的条件。

参考思路

第一版必须包含身份、归属、上传、状态查询与删除;暂不做 PDF OCR、多 Agent 和复杂权限编辑器。验收覆盖有效输入、无效输入、两个用户、重复请求、文件失败、数据库失败、后台失败、重启恢复。进入下一版前,应能按说明在本机或本地容器独立启动,并完成权限、失败和重启恢复验证。备份恢复实验可在隔离的本地测试数据库进行;无需公网域名、云部署或网站发布。仅看过页面仍不足以证明这些行为成立。

验收与自测#

  • 四个版本各自有可演示的完整用户流程。
  • 能提供权限、失败恢复与评测的具体证据。
  • 能在新环境按 README 启动并解释配置。
  • 能说清尚未完成的生产能力,避免夸大作品范围。

问:加入 Agent 就代表项目高级吗? 答:价值取决于是否解决任务,可靠性和可解释性更重要。

问:为什么第一版不接模型? 答:便于先确认普通全栈基础,也可以减少排错变量。

问:有漂亮界面和聊天功能就能验收吗? 答:还需要数据、权限、失败恢复、可重复本地启动和质量证据;公网发布不是本次实训条件。

官方参考#

原有课程整理于 2026-09-10;Node / Electron 扩充于 2026-09-11。示例环境与验证范围以正文为准。
原创中文学习手册,阅读结构参考 Vue 文档;非 Vue 官方教材。
下载本章 Markdown

支持中文和英文全文搜索 · ↑ ↓ 选择 · Enter 打开 · Esc 关闭