综合项目:团队知识助手
按四个可验收版本串起前端、后端、知识库和业务工具,形成可展示的作品。
建议先读:需求拆解与方案设计
本页内容
项目目标:团队知识与任务助手#
这个综合项目把手册中的能力连接成一条产品交付链:用户登录,上传有权限的资料,查看处理状态,提出问题,核对引用;在需要时,让助手生成任务草稿,经确认后执行。目标是 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。样例记录是明确标注的教学数据,实际项目应替换为真实测试记录。
// 教学记录:这里只演示报告校验,并不代表真实项目已通过。
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 服务、数据库、向量模型或真实生成能力,不能视为综合项目成品。
// 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 就代表项目高级吗? 答:价值取决于是否解决任务,可靠性和可解释性更重要。
问:为什么第一版不接模型? 答:便于先确认普通全栈基础,也可以减少排错变量。
问:有漂亮界面和聊天功能就能验收吗? 答:还需要数据、权限、失败恢复、可重复本地启动和质量证据;公网发布不是本次实训条件。