Skip to content

Monorepo 公共逻辑

要点

  • 公共逻辑优先承载跨端契约,例如任务状态、资料结构、审稿问题、Agent 输出 schema 和 API 类型。
  • Agent 项目的共享包要服务于 Web 用户工作台、Admin 管理后台、API / Agent 服务之间的协作语言。
  • 页面交互、后台筛选、Agent 内部步骤和数据库实现留在各自应用或模块里,避免共享包扩张成业务杂物箱。

1. 公共逻辑的边界

上一篇文章确定了三端架构:Web 用户工作台、Admin 管理后台、API / Agent 服务。进入 Monorepo 之后,最容易犯的错误是把公共逻辑理解成「只要两个地方用到,就抽到 packages」。

更稳妥的判断标准可以放在「契约」上:一段代码如果会同时约束 Web 展示、Admin 审计、API 校验和 Agent 输出,就适合进入共享包;如果只影响某个页面或某个 Agent 节点的内部实现,先留在本地。

Agent 项目里,适合共享的内容通常有:

  • 任务状态枚举。
  • 资料卡片结构。
  • 审稿问题结构。
  • Agent 输出 schema。
  • API 请求和响应类型。
  • Prompt 模板 ID 和版本。
  • 共享 UI 组件。
  • TypeScript、ESLint、Prettier 配置。

不适合过早共享的内容包括:

  • 某个页面的表单逻辑。
  • 某个后台列表的筛选状态。
  • 某个 Agent 节点内部的实现步骤。
  • 只被一个应用使用的业务 helper。

这个边界能减少一个常见问题:共享包一旦混入太多局部实现,任何用户工作台的小改动都可能影响后台审计或 Agent 执行链路。共享包应该让三端对同一件事使用同一套字段和状态,业务编排仍然交给各自模块处理。

2. 推荐目录结构

一个可维护的结构可以先从下面开始:

txt
apps/
  web/
  admin/
  api/

packages/
  contracts/
  prompts/
  ui/
  config/

其中:

  • contracts 保存跨端数据契约,包括任务状态、资料卡片、审稿问题和 Agent 输出结构。
  • prompts 保存 Prompt 模板元信息、版本、变量定义和输出 schema 关联。
  • ui 保存基础组件,不保存具体业务工作流页面。
  • config 保存 TypeScript、ESLint、Prettier 等配置。

在 pnpm 工作区里,内部包之间的依赖建议写成 workspace:*,这样无论本地版本号怎么变,引用方始终拿到当前工作区的最新代码。Turborepo 会根据 package.json 中的依赖关系自动构建任务顺序,例如 apps/web 依赖 packages/ui 时,构建 web 之前会先完成 ui 的构建。

每个共享包的 package.json 最好显式声明 exports 字段,把公开入口和内部文件分开。contracts 可以只导出类型文件,ui 可以为每个基础组件提供独立子路径,这样消费方只能按约定导入,避免直接引用内部实现。

共享包的职责应该少而稳定。只要一个包开始依赖具体页面、具体路由、具体数据库实现,就说明边界已经向业务实现滑动。业界常见建议是先保持 3–5 个包,等有明确的独立发布或独立构建需求再拆分,避免过早拆出大量几乎不会被单独消费的微型包。

3. contracts:系统契约中心

contracts 是最重要的共享包。它定义 Web、Admin、API 和 Agent 节点都要共同理解的数据结构。

例如任务状态、审稿问题和资料卡片:

typescript
export type TaskStage =
  | "intake"
  | "research"
  | "plan"
  | "execute"
  | "review"
  | "rewrite"
  | "delivery_check"
  | "done"
  | "blocked";

export type ReviewFinding = {
  id: string;
  severity: "P0" | "P1" | "P2";
  title: string;
  body: string;
  targetSection?: string;
  suggestedAction?: "rewrite" | "fact_check" | "source_required" | "remove";
  resolved: boolean;
};

export type SourceCard = {
  id: string;
  title: string;
  url?: string;
  sourceType: "official" | "project" | "paper" | "secondary";
  reliability: "high" | "medium" | "low";
  usedInSections: string[];
};

这些类型不只服务于 TypeScript 补全。它们也是系统里的协作语言:Web 用 TaskStage 展示任务进度,Admin 用 ReviewFinding 统计质量问题,API 用 SourceCard 校验资料引用,Agent 用同一组字段组织结构化输出。

4. prompts:共享模板元信息

Prompt 也需要共享,但共享方式要克制。更适合共享的是模板 ID、版本、变量 schema、输出 schema 和适用场景。完整 Prompt 文本可以按 Agent 与版本分文件管理,避免所有研究、规划、执行、审稿模板挤进一个巨型常量文件。

typescript
export type PromptTemplateMeta = {
  id: "research_agent" | "plan_agent" | "execute_agent" | "review_agent";
  version: string;
  variables: string[];
  outputSchema: string;
  appliesToStage: TaskStage;
};

这样 Web 可以展示当前任务使用的 Prompt 版本,Admin 可以管理不同版本的启停,API 可以在 trace 中记录 research_agent@2026-07-17 这类版本信息。后续排查「资料不足」「审稿建议重复」「改写偏离原意」时,版本号能直接连接到对应模板和输出 schema。

5. ui:只放基础组件

Web 和 Admin 都会用到按钮、表格、弹窗、标签、状态徽标等组件。把这些放到 packages/ui 有价值。

但业务组件要谨慎共享。例如「审稿问题列表」看起来两个端都用,但 Web 端关注用户如何处理问题,Admin 端关注问题分布、命中规则和审计记录,两者交互目标不同。可以共享底层 IssueBadgeSeverityTagTaskStageBadge,完整列表组件更适合留在各自应用里。

共享 UI 的原则是:越基础越适合共享,越接近业务流程越应该留在应用内。

需要注意框架依赖的声明方式。packages/ui 这类共享组件包通常不应该把 React 列为自己的 dependencies,而应该列为 peerDependencies。如果 ui 自己安装了一份 React,而 apps/web 又安装了一份 React,运行时就可能出现两个 React 实例,导致 "hooks can only be called inside the body of a function component" 这类错误。peerDependencies 让消费方提供 React 版本,内部包只复用同一份。

6. config:让工程规则一致

config 包保存项目级工程规则,例如:

  • TypeScript 严格模式配置。
  • ESLint / Biome / Prettier 规则。
  • 测试基础配置。
  • 构建目标配置。

这类配置本身没有业务价值,但能减少多端风格漂移。Agent 项目会长期维护大量 Prompt、schema 和 TypeScript 代码,工程规则一致会降低后续审查成本。

对 Agent 项目来说,工程规则还会影响 Agent 输出物的可维护性。Prompt、schema 和测试夹具如果采用同一套格式约束,代码审查时更容易把注意力放回字段语义、版本变化和任务状态迁移。

7. 避免共享包失控

共享包失控通常有几个信号:

  • contracts 开始 import 数据库实现。
  • ui 开始依赖某个具体页面路由。
  • prompts 里混入运行时代码。
  • config 以外的包互相循环依赖。
  • 一个 helper 只被一个地方使用,却被提前抽到 shared。
  • package.json 没有 exports 字段,导致外部代码可以随意引用内部文件。
  • 共享组件包把 React 列为 dependencies 而不是 peerDependencies

发现这些信号时,不要继续抽象。先把代码放回使用方,等第二个稳定场景出现后再共享。

当共享包数量增加、需要独立发布或对外部使用者暴露版本时,可以用 Changesets 管理版本和 changelog。它通过 .changeset 目录下的变更声明,在发布时自动计算每个包的 semver 版本、更新依赖它的内部包版本,并生成每个包的 changelog。对于 Agent 项目,contractsprompts 的字段变更往往同时影响 Web、Admin、API 三端,Changesets 能帮你把这些关联包的版本变化一起表达出来。

在 Agent 项目里,还要额外注意 Prompt 和 Agent 运行时的边界。contracts 可以定义 ReviewFinding,但不应该知道审稿 Agent 如何逐段扫描产物;prompts 可以登记模板版本,但不应该直接调用模型;ui 可以展示状态徽标,但不应该决定任务如何从 review 进入 rewrite

8. 小结

Monorepo 的公共逻辑应该服务于契约一致,避免只因为两处代码相似就提前共享。

对 Agent 项目来说,优先共享任务状态、资料卡片、审稿问题、Agent 输出 schema、Prompt 元信息和基础 UI。业务流程、页面交互和 Agent 内部实现先保持局部化。这样共享包会更稳定,三端协作也更容易长期维护。

参考资料

基于 MIT 协议开源