# 薪火 AI · 架构设计

> 上一份 `RESEARCH.md` 比较了选项。这一份选定了路径并给出实现细节。

---

## 一、最终选定的架构

```
┌──────────────────────────────────────────────────────────────────────────┐
│                            浏览器（孩子的电脑）                             │
│                                                                          │
│   薪火页面 ──▶ window.kindlingAI.chat({ system: ..., user: ... })        │
│              ──▶ window.kindlingAI.judge(...)                            │
│              ──▶ window.kindlingAI.image(...)                            │
│              ──▶ window.kindlingAI.transcribe(audio)                     │
│              ──▶ window.kindlingAI.embed(texts)                          │
│                                                                          │
│   这些函数都在 kindling-ai-sdk.js 里，完全自动处理流式、错误、降级、缓存。   │
└──────────────────────────────────────────────────────────────────────────┘
                              │  HTTPS POST
                              │  no auth needed (rate-limited by IP)
                              ▼
┌──────────────────────────────────────────────────────────────────────────┐
│              Cloudflare Worker · ai.kindling-edu.com                     │
│              （或 Vercel Edge Function 备选）                            │
│                                                                          │
│   POST /v1/chat       → router → Groq（默认）/ OpenRouter（升级）       │
│   POST /v1/judge      → router → Groq Llama-3.3-70B                     │
│   POST /v1/image      → router → Cloudflare Workers AI Flux             │
│   POST /v1/embed      → router → HuggingFace bge-small-zh               │
│   POST /v1/transcribe → router → Groq Whisper Large v3                  │
│                                                                          │
│   防护层：                                                                │
│   - per-IP rate limit (KV: 30/min, 500/day)                              │
│   - per-day cost cap ($30/天，超了所有请求改走 Groq 免费层)               │
│   - response size limit (max_tokens 500/1500/4000，按 stage)             │
│   - prompt injection 简单关键词过滤                                       │
│   - CORS only kindling-edu.com（防别人偷接口）                           │
└──────────────────────────────────────────────────────────────────────────┘
                              │
              ┌───────────────┼───────────────────────┐
              ▼               ▼                       ▼
       ┌──────────┐    ┌─────────────┐         ┌──────────────┐
       │   Groq   │    │ OpenRouter  │         │ Cloudflare   │
       │  (默认)  │    │   (升级)    │         │ Workers AI   │
       │ Llama 70B│    │  GPT/Claude │         │ Flux 图像生成 │
       │  免费    │    │   /Qwen     │         │              │
       └──────────┘    └─────────────┘         └──────────────┘
```

---

## 二、前端 SDK 接口（kindling-ai-sdk.js）

孩子和老师不需要懂这些细节。但页面写代码的人（我们）调用这一套。

```js
// 文字聊天
const response = await kindlingAI.chat({
  system: "你是 9 岁孩子的恐龙小专家。规则：……",
  user:   "霸王龙能跑多快？",
  stream: true,                  // 是否流式
  onToken: (t) => display(t),    // 流式回调
  stage:  'middle'               // 'sprouts' / 'middle' / 'builders' 决定 max_tokens
});
// → { content: "...", model: "llama-3.3-70b", tokens: 287, ms: 1240 }

// 多轮对话
const response = await kindlingAI.chat({
  system: "你是……",
  history: [
    { role: 'user',      content: '你好' },
    { role: 'assistant', content: '你好呀小朋友' },
  ],
  user: "今天讲恐龙好吗？",
});

// 判官 AI（审美工作室）
const score = await kindlingAI.judge({
  rubric: "5 条评分标准：……",
  candidate: "AI 回答的内容",
  reference: "好的回答应该长这样",  // 可选
});
// → { score: 8.5, breakdown: {...}, reasoning: "……" }

// 图像生成
const url = await kindlingAI.image({
  prompt: "一只霸王龙在森林里",
  style: 'cartoon',  // 'photo' / 'illustration' / 'pixel'
  size: '1024x1024',
});
// → "data:image/png;base64,..."

// 看图说话
const desc = await kindlingAI.vision({
  imageBase64: "data:image/png;base64,...",
  question: "这张画是什么？",
});

// 语音输入
const text = await kindlingAI.transcribe({
  audio: blob,         // 浏览器录的
  language: 'zh',
});

// RAG / embedding
const vectors = await kindlingAI.embed({
  texts: ["第一段文本", "第二段文本", ...]
});
// → [[0.1, 0.2, ...], [0.3, 0.4, ...], ...]
```

---

## 三、Worker 的核心代码骨架（伪代码）

```ts
// worker.ts
export default {
  async fetch(req: Request, env: Env): Promise<Response> {
    const url = new URL(req.url);

    // CORS
    if (req.method === 'OPTIONS') return preflight();
    if (!isAllowedOrigin(req)) return new Response('forbidden', { status: 403 });

    // Rate limit by IP（Cloudflare Workers KV）
    const ip = req.headers.get('CF-Connecting-IP');
    const rate = await env.KV.get(`rate:${ip}`);
    if (rate && parseInt(rate) > 30) return new Response('too many', { status: 429 });
    await env.KV.put(`rate:${ip}`, String((parseInt(rate || '0')) + 1), { expirationTtl: 60 });

    // 每天总开销保护
    const cost_today = await env.KV.get('cost:today');
    const force_free = parseFloat(cost_today || '0') > 30;

    // 路由
    if (url.pathname === '/v1/chat')      return handleChat(req, env, force_free);
    if (url.pathname === '/v1/judge')     return handleJudge(req, env, force_free);
    if (url.pathname === '/v1/image')     return handleImage(req, env);
    if (url.pathname === '/v1/embed')     return handleEmbed(req, env);
    if (url.pathname === '/v1/transcribe')return handleTranscribe(req, env);
    return new Response('not found', { status: 404 });
  }
}

async function handleChat(req, env, force_free) {
  const { system, user, history, stream, stage } = await req.json();

  // 选模型
  const model = force_free
    ? { provider: 'groq', name: 'llama-3.3-70b-versatile' }
    : pickModel(stage);  // stage='builders' → 'gpt-4o-mini'

  // 构造请求
  const messages = [
    { role: 'system', content: system },
    ...(history || []),
    { role: 'user', content: user }
  ];

  const upstream = await fetch(model.endpoint, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${env[model.keyName]}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      model: model.name,
      messages,
      stream: !!stream,
      max_tokens: stage === 'sprouts' ? 500 : stage === 'middle' ? 1500 : 4000,
    }),
  });

  // Track cost (estimate)
  await trackCost(env, estimateCost(messages, model));

  // Stream back if requested, else return JSON
  if (stream) return upstream;  // pipe through
  return new Response(upstream.body, { headers: { 'content-type': 'application/json' } });
}
```

---

## 四、每个 AI 能力的具体路径

### 1. 文字聊天 / system prompt
**路径**：浏览器 → /v1/chat → Groq Llama-3.3-70B（默认）
**为什么 Groq**：免费 30 req/min/IP，速度极快（200+ tok/s），中英文都行
**升级**：当 stage='builders' 或检测到中文比例 > 70% → 走 OpenRouter Qwen-Plus

### 2. 判官 AI / 评分（项目 11、12）
**路径**：浏览器 → /v1/judge → Groq Llama-3.3-70B
**Prompt 模板**：自带一个标准 LLM-as-judge 模板，孩子只要给 rubric + candidate
**输出**：JSON 格式 `{ score, breakdown, reasoning }`，自动解析

### 3. 图像生成（萌芽版"画一画"）
**路径**：浏览器 → /v1/image → Cloudflare Workers AI Flux-1-Schnell
**为什么 CF**：每天 50 张图免费，模型质量足够，无需注册新账号（worker 直接调）
**返回**：base64 PNG，前端直接显示

### 4. 看图说话（萌芽版"AI 看你画的"）
**路径**：浏览器 → /v1/vision → OpenRouter Qwen2.5-VL（或 GPT-4o-mini）
**输入**：base64 图 + 一个问题
**用途**：孩子画完一只恐龙，AI 评论 + 给写作灵感

### 5. 语音输入
**两层**：
- **首选**：浏览器原生 `Web Speech API`（零成本、零延迟）
- **降级**：浏览器录音 → 上传 → /v1/transcribe → Groq Whisper Large v3

### 6. Embedding / RAG（项目 01、11）
**路径**：浏览器 → /v1/embed → HuggingFace `bge-small-zh-v1.5`
**向量库**：浏览器内 IndexedDB（< 1000 文档够用），用余弦相似度查 top-k
**进阶版**：可对接 Pinecone serverless（免费层 1 GB）

---

## 五、错误处理 + 降级链

```
chat() 被调用
   │
   ▼
尝试 1：Groq
   │ ✅ 成功 → 返回
   │ ❌ 429 限流 → 等 1 秒重试一次
   │ ❌ 仍失败
   ▼
尝试 2：OpenRouter Llama-3.3 :free
   │ ✅ 成功 → 返回
   │ ❌ 失败
   ▼
尝试 3：浏览器内 WebLLM (Qwen 1.5B)
   │ ✅ 成功 → 返回（标注"用了备用 AI，可能慢一点"）
   │ ❌ 失败
   ▼
最后：友好错误信息 → "AI 暂时不可用，请稍后重试。或者复制下面的 prompt 到 Qwen Chat 自己跑。"
+ 自动给孩子准备好的 prompt 文本 + 一键复制 + 一键跳 Qwen
```

---

## 六、UI 集成模式

每个项目页都用同一个组件：`<div class="ai-runner" data-system="..." data-stage="middle">`

它会渲染成：
```
┌─────────────────────────────────────────┐
│  [输入框：写你的问题]                     │
│  [运行 ▶]    估计响应：3 秒              │
│  ┌─────────────────────────────────┐    │
│  │ AI 回答（流式打出来）：           │    │
│  │ 哎呀，糖醋排骨啊...              │    │
│  └─────────────────────────────────┘    │
│  [👍 这次答得好]  [👎 答得不好]          │
│  [📋 复制]  [🔄 再试]  [⚙️ 改 prompt]   │
└─────────────────────────────────────────┘
```

孩子改 system prompt → 立刻可以再跑一次看效果。这是核心循环。

---

## 七、数据 / 隐私

- **不存对话内容到任何永久存储**（worker 只做转发）
- **存的只有：IP+时间用于限流，token 数用于成本统计**
- **localStorage 里存孩子自己的 prompt 历史**（端到端在浏览器，不传服务器）
- 老师模式可选：导出班级集体使用统计（聚合数据）

---

## 八、为什么这个架构"对"

1. **零孩子门槛** —— 完全没注册、没 key 输入、没跨站
2. **成本可控** —— 默认免费层兜底，只在必要时付费
3. **能力齐全** —— 5 大能力（chat / judge / image / vision / voice）一个 SDK 通调
4. **降级稳健** —— 三层 fallback，最坏情况还是能用
5. **教学忠实** —— Worker 用的还是开源模型（Llama / Qwen），符合"开源 AI 让全世界孩子能用"的初心

---

## 九、本仓库其它文件

- `RESEARCH.md` — 选项对比（你刚读完的）
- `ARCHITECTURE.md` — 本文档
- `prototype-chat.html` — 能跑的"点一下就跑"原型，文字聊天
- `prototype-judge.html` — 判官 AI 原型
- `prototype-image.html` — 图像生成原型
- `prototype-voice.html` — 语音输入原型
- `kindling-ai-sdk.js` — 前端 SDK 雏形
- `worker/worker.ts` — Cloudflare Worker 雏形
- `INTEGRATION.md` — 怎么把这套接进现有 100 多页项目
