# 综合项目：团队知识助手

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

这个综合项目把手册中的能力连接成一条产品交付链：用户登录，上传有权限的资料，查看处理状态，提出问题，核对引用；在需要时，让助手生成任务草稿，经确认后执行。目标是 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，语义检索尚未接入，公网入口未配置”。这种说明让评审知道演示实际证明什么，也帮助你安排下一步学习。未公开部署不等于作品无效，夸大模拟能力才会破坏可信度。

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

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

<details><summary>完整参考交付说明</summary>

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

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

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

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

</details>


## 练习与参考答案

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

<details>
<summary>参考思路</summary>

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

</details>

## 验收与自测

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

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

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

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

## 官方参考

- [Vue：应用规模化](https://cn.vuejs.org/guide/scaling-up/state-management)
- [PostgreSQL：事务](https://www.postgresql.org/docs/current/tutorial-transactions.html)
- [OWASP：LLM 应用风险](https://genai.owasp.org/llm-top-10/)
- [Anthropic：有效 Agent](https://www.anthropic.com/engineering/building-effective-agents)
