AI 能生成 PPT 后,决策页也该有 CI

生成 PPT 的 pipeline 里,最容易被忽略的是一个事实:slide 可以生成,decision 不会因此成立。

例如模型返回:

text 复制代码
建议优先推进方案 B。

从渲染器看,这已经是合格的标题和要点;从决策看,它甚至还没有说清"希望谁批准什么"。

所以可以把决策页当成一个需要 CI 的构建输入:页面文案负责展示,判断链负责决定这页有没有资格进入渲染。

1. 给决策页一个旁路元数据文件

不建议把所有字段塞进 slide 的正文,也不建议强迫背景页使用同一套约束。把需要行动的页面标为 decision,再给它一个同名 trace 文件即可。

ts 复制代码
type Evidence = {
  id: string;
  kind: 'fact' | 'assumption';
  excerpt: string;
  locator: string;
};

type PassiveTrace = {
  slideId: string;
  kind: 'context' | 'status';
};

export type DecisionTrace = {
  slideId: string;
  kind: 'decision';
  claim: string;
  requestedDecision: string;
  evidence: Evidence[];
  rejectedOptions: string[];
  reconsiderWhen: string[];
  reviewOwner: string;
};

export type SlideTrace = PassiveTrace | DecisionTrace;

locator 可以是文档锚点、数据集版本、查询保存位置或人工材料编号。它不是为了做"引用格式",而是让后来的人有地方可回。

2. 规则只拦一种错误:结论页没有判断链

ts 复制代码
const nonEmpty = (value: unknown): value is string =>
  typeof value === 'string' && value.trim().length > 0;

const nonEmptyList = (value: unknown): boolean =>
  Array.isArray(value) && value.some(nonEmpty);

const isRecord = (value: unknown): value is Record<string, unknown> =>
  typeof value === 'object' && value !== null;

export function validateDecisionTrace(trace: unknown): string[] {
  const errors: string[] = [];

  if (!isRecord(trace)) return ['trace must be an object'];

  if (!nonEmpty(trace.slideId)) errors.push('missing slideId');
  if (trace.kind !== 'context' && trace.kind !== 'status' && trace.kind !== 'decision') {
    errors.push('kind must be context, status, or decision');
    return errors;
  }
  if (trace.kind !== 'decision') return errors;

  if (!nonEmpty(trace.claim)) errors.push('decision slide needs claim');
  if (!nonEmpty(trace.requestedDecision)) errors.push('decision slide needs requestedDecision');
  const evidence = Array.isArray(trace.evidence) ? trace.evidence : [];
  if (!Array.isArray(trace.evidence)) errors.push('decision slide needs evidence');
  evidence.forEach((item, index) => {
    if (!isRecord(item) || !nonEmpty(item.id) || !nonEmpty(item.excerpt) || !nonEmpty(item.locator)) {
      errors.push(`evidence[${index}] needs id, excerpt, and locator`);
    }
  });
  if (!evidence.some((item) => isRecord(item) && item.kind === 'fact' && nonEmpty(item.excerpt))) {
    errors.push('decision slide needs one fact evidence');
  }
  if (!nonEmptyList(trace.rejectedOptions)) errors.push('decision slide needs rejectedOptions');
  if (!nonEmptyList(trace.reconsiderWhen)) errors.push('decision slide needs reconsiderWhen');
  if (!nonEmpty(trace.reviewOwner)) errors.push('decision slide needs reviewOwner');

  return errors;
}

export function assertValidDecisionTrace(trace: unknown): asserts trace is SlideTrace {
  const errors = validateDecisionTrace(trace);
  if (errors.length > 0) throw new Error(errors.join('\n'));
}

这里允许模型写 assumption,但不允许它把假设伪装成唯一依据。也允许团队暂时不知道最终答案,但不允许一个请求行动的页面没有撤回条件。

3. 让失败结果像 CI 一样明确

下面是示例,不是业务数据:

ts 复制代码
// 模型或文件读取到的是未受信任输入,不该先假定它满足 DecisionTrace。
const draft: unknown = {
  slideId: 'strategy-03',
  kind: 'decision',
  claim: '建议优先推进方案 B。',
  evidence: [
    {
      id: 'model-summary',
      kind: 'assumption',
      excerpt: 'B 可能有更大的增长空间。',
      locator: 'generated-summary',
    },
  ],
};

console.log(validateDecisionTrace(draft));
txt 复制代码
[
  'decision slide needs requestedDecision',
  'decision slide needs one fact evidence',
  'decision slide needs rejectedOptions',
  'decision slide needs reconsiderWhen',
  'decision slide needs reviewOwner'
]

这个报错没有评价 B 对不对。它只在说:在补齐这些信息前,请把这页当作讨论草稿,而不是可以进入发布流程的结论页。

4. 用一个最小测试防止规则被悄悄删掉

ts 复制代码
const assert = require('node:assert/strict');
const { validateDecisionTrace } = require('./decision-trace.js');

const approved = {
  slideId: 'strategy-03',
  kind: 'decision',
  claim: 'B 值得先在目标人群中做限定范围验证。',
  requestedDecision: '批准限定范围验证,不批准全面切换。',
  evidence: [
    {
      id: 'cohort-report-v2',
      kind: 'fact',
      excerpt: '目标人群完成率高于当前基线。',
      locator: 'report.md#target-cohort',
    },
  ],
  rejectedOptions: [
    'A 缺少后续完成证据。',
    'C 缺少同口径对照。',
  ],
  reconsiderWhen: [
    '下一轮目标人群完成率不再高于当前基线。',
  ],
  reviewOwner: '业务负责人,在下一次复核决定是否扩大。',
};

assert.deepEqual(validateDecisionTrace(approved), []);
assert.match(
  validateDecisionTrace({ ...approved, reviewOwner: '' }).join('\n'),
  /reviewOwner/
);

这里的成功信号不是"生成了一套 PPT",而是两件事:完整 trace 能通过;任何人删掉负责人、事实引用或撤回条件时,构建测试会失败。

5. 让它真的跑在 CI 里

上面的断言只有进入每次构建,标题里的 CI 才不是比喻。下面假设 TypeScript 已由项目现有构建步骤编译到 dist/:

json 复制代码
{
  "scripts": {
    "test:decision-traces": "node --test dist/decision-trace.test.js",
    "check:decision-traces": "node dist/check-decision-traces.js",
    "build:ppt": "node dist/build-deck.js"
  }
}

在现有 CI 工作流的构建步骤前加上:

yaml 复制代码
- run: npm ci
- run: npm run test:decision-traces
- run: npm run check:decision-traces
- run: npm run build:ppt

check-decision-traces.js 读取当天要渲染的 trace 文件,任一决策页缺字段即以非零退出。这样,页面不会在评审结束后才暴露"没有人知道依据在哪",而是无法进入 PPT 构建产物。

最小检查脚本可以保持很短:

ts 复制代码
const { readFileSync } = require('node:fs');
const { validateDecisionTrace } = require('./decision-trace.js');

const loaded = JSON.parse(
  readFileSync('slides/traces.json', 'utf8')
);

if (!Array.isArray(loaded)) {
  throw new Error('slides/traces.json must contain an array');
}

const failures = loaded.flatMap((trace, index) =>
  validateDecisionTrace(trace).map((error) => `trace[${index}]: ${error}`)
);

if (failures.length > 0) {
  console.error(failures.join('\n'));
  process.exit(1);
}

slides/traces.json 中的每条记录与待渲染页面按 slideId 对应。项目如果把 trace 分文件保存,只需把读取部分换成目录遍历,校验和退出码逻辑不变。

6. 接到渲染前,而不是评审后

ts 复制代码
type BuildInput = {
  trace: unknown;
  render: (trace: SlideTrace) => Promise<void>;
};

export async function buildDeck(input: BuildInput[]): Promise<void> {
  for (const slide of input) {
    assertValidDecisionTrace(slide.trace);
    await slide.render(slide.trace);
  }
}

执行环境是 Node.js 18+、项目已有的 TypeScript 编译步骤,以及任意现有渲染器。trace 可以由支持结构化输出的模型生成,也可以先由人写在同目录 JSON/YAML 文件中。没有自动化 PPT 管线时,把同样字段放进 Markdown 备注,评审前运行校验,也能得到第一步收益。

边界同样要写清:这个校验器不验证证据是否真实、引用是否完整、替代方案是否公平,也不保证负责人真的会回来复核。它只把一种常见的流程漏洞变成可见失败:一页想推动行动的内容,不能只靠一句生成出来的结论通过构建。

当 AI 把渲染页面变得便宜,CI 最该保护的就不只是格式和文件是否生成成功,也应该包括这页是否还保留了人需要承担的判断。

相关推荐
七牛云行业应用6 小时前
MIT “权重空间编程”:GPT-6 Astra 修改 0.016% 的 Qwen 权重,MATH-500 从 7.9% 升至 48.1%
人工智能·gpt·ai
一缕82年的清风18 小时前
智能体“自适应慢思考”的算力黑洞:当 Claude 5.5 与 GPT-6.1 Sol 陷入推理自旋,为什么高并发架构必须重构“认知预算熔断与快慢推测执行”?
gpt·claude·gemini
AI 编程助手GPT2 天前
GPT 排的日志时间线差一小时?用 zoneinfo 复现夏令时回拨
人工智能·python·gpt
阿愚先生2 天前
【从零手写GPT(一)】:RTX 5080 环境踩坑,我装错了两次 PyTorch
人工智能·pytorch·gpt
一缕82年的清风2 天前
智能体“自主决策”的虚假繁荣:当 Claude 5.5 与 GPT-6.1 深入生产,为什么顶级架构师都在重回“确定性状态机”?
gpt·claude
AI 编程助手GPT2 天前
GPT 改完配置别直接覆盖:用 git diff 审查两份文件
git·gpt
一缕82年的清风3 天前
算力平民化神话破产:当推理成本暴降 90%,为什么生产级 Agentic 架构反而更贵更脆弱了?
gpt·claude
西索斯coding3 天前
gpt-image-2.5-sunburst 和 gemini-3.1-flash-image 差多少?60 组项目素材的成功成本评测框架
java·linux·gpt·ai
一枕槐安4873 天前
大模型 API 调用封装:统一适配 GPT、文心一言、通义千问
人工智能·gpt·文心一言·大模型api·流式输出·统一封装·多模态路由
最强小杰4 天前
gpt-image-2.5-sunburst + OpenAI SDK + Cline 怎么接?完整图生图与局部重绘教程
gpt·ios·ai·iphone