💻 源码
# 04 · 运行流程详解

本文按时间顺序说明:**从安装到点击「运行演示」,系统内部发生了什么**。

---

## 一、环境准备流程

```mermaid
flowchart LR
    A[安装 Node.js] --> B[npm install]
    B --> C[配置 DASHSCOPE_API_KEY]
    C --> D[npm run dev]
    D --> E[浏览器打开 localhost:5173]
```

### 步骤说明

| 步骤 | 命令/操作 | 说明 |
|------|-----------|------|
| 1 | 安装 Node.js 18+ | https://nodejs.org/ |
| 2 | `npm install` | 安装 React、Vite、TypeScript 等依赖 |
| 3 | 配置 API Key | 页面输入,或系统环境变量 `DASHSCOPE_API_KEY` |
| 4 | `npm run dev` | 启动 Vite,默认端口 5173 |
| 5 | 打开浏览器 | 访问 http://localhost:5173 |

### 开发服务器启动后

- Vite 编译 `src/` 下的 React 代码
- 注册代理:浏览器访问 `/api/dashscope/*` → 转发到 `https://dashscope.aliyuncs.com/*`

---

## 二、用户点击「运行演示」后的流程

### 2.1 总览

```mermaid
flowchart TD
    Start[用户点击运行演示] --> Validate{API Key 和问题是否有效?}
    Validate -->|否| Error[显示错误提示]
    Validate -->|是| Parallel[并行启动两个任务]

    Parallel --> Plain[普通模式 plainChat]
    Parallel --> React[ReAct 模式 runReAct]

    Plain --> PlainDS[请求 DashScope<br/>不带 tools]
    React --> ReactDS[请求 DashScope<br/>带 tools]

    ReactDS -->|有 tool_calls| Exec[executeTool 执行工具]
    Exec --> ReactDS2[再次请求 DashScope<br/>带上 tool 结果]
    ReactDS -->|无 tool_calls| Answer[生成最终回答]

    PlainDS --> PlainUI[更新左侧 PlainChain]
    ReactDS2 --> ReactUI[更新右侧 ReActChain]
    Answer --> ReactUI
```

### 2.2 对比模式下的并行执行

`App.tsx` 中使用 `Promise.all` 同时运行:

```ts
const [reactResult, plainResult] = await Promise.all([
  runReAct(key, question, onStep),   // 右侧
  plainChat(key, question, onStep),  // 左侧
])
```

这样学生能同时看到两种模式的耗时和结果差异。

---

## 三、ReAct 模式逐步拆解(以计算题为例)

**用户问题**:`请计算 12345 × 6789 等于多少?`

### 第 0 步:记录用户输入

| 步骤类型 | 内容 |
|----------|------|
| input | 用户提问原文 |

### 第 1 轮推理

#### ① 请求大模型(带 tools)

发送内容:

- `system`:ReAct 规则提示词
- `user`:用户问题
- `tools`:四个工具的定义

#### ② 模型返回 tool_calls

```json
{
  "tool_calls": [{
    "id": "call_xxx",
    "function": {
      "name": "calculate",
      "arguments": "{\"expression\": \"12345 * 6789\"}"
    }
  }]
}
```

#### ③ 界面展示 Thought

`thought.ts` 生成类似:

```text
【第 1 轮思考】
用户问题包含精确计算……大模型心算容易出错,应交给计算工具。
→ 结论:调用工具「数学计算」(calculate)
→ 参数:expression=12345 * 6789
```

#### ④ 界面展示 Action

```text
调用工具:数学计算(calculate)
参数:{"expression": "12345 * 6789"}
```

#### ⑤ 执行工具 → Observation

`executeTool('calculate', ...)` 本地计算:

```text
12345 * 6789 = 83810205
```

#### ⑥ 把结果追加到 messages

```json
{
  "role": "tool",
  "tool_call_id": "call_xxx",
  "content": "12345 * 6789 = 83810205"
}
```

### 第 2 轮推理

#### ⑦ 再次请求大模型

此时 messages 已包含:system、user、assistant(tool_calls)、tool(结果)。

#### ⑧ 模型返回最终文本(无 tool_calls)

#### ⑨ 界面展示 Thought + Answer

```text
【第 2 轮思考】
已收到工具返回的真实数据……信息足够,可以生成最终答案。
```

```text
12345 × 6789 = 83,810,205
```

### 完整链条示意

```text
❓ 用户提问
    ↓
💭 第1轮 Thought  →  ⚡ Action(calculate)  →  👁 Observation(83810205)
    ↓
💭 第2轮 Thought  →  ✅ Answer
```

---

## 四、普通模式逐步拆解

| 步骤 | 界面显示 | 背后发生的事 |
|------|----------|--------------|
| 1 | 用户提问 | 记录 input |
| 2 | 跳过 ReAct 循环 | 静态说明:无 T/A/O |
| 3 | 直接输出 | 一次 `chatCompletion`,**不传 tools** |
| 4 | 最终回答 | 模型凭训练知识生成,可能心算或编造 |

---

## 五、消息列表(messages)的变化

ReAct 模式下,`messages` 数组会**不断增长**:

```mermaid
sequenceDiagram
    participant M as messages 数组

    Note over M: [system, user]
    M->>M: + assistant (含 tool_calls)
    M->>M: + tool (计算结果)
    Note over M: 第二次请求
    M->>M: + assistant (最终回答)
```

这正是 Function Calling 的标准模式:**工具结果必须回传给模型**,模型才能基于真实数据回答。

---

## 六、界面实时更新机制

`runReAct` 接受回调 `onStep`:

```ts
runReAct(apiKey, question, (step) => {
  setReactSteps((prev) => [...prev, step])
})
```

每产生一个 Thought / Action / Observation / Answer,立刻追加到 state,**不用等全部完成**,所以学生能看到「实时推理」效果。

`groupStepsIntoRounds()` 把扁平 steps 按 `round` 字段分组,供 `ReActChain` 按轮渲染。

---

## 七、命令行测试流程

### 7.1 只测工具(无需大模型)

```bash
node scripts/test-tools.mjs
```

```
✅ calculate
✅ get_current_datetime
✅ get_exchange_rate
✅ get_weather
工具测试: 4/4 通过
```

### 7.2 测完整 ReAct 链路

```bash
# 需先设置环境变量 DASHSCOPE_API_KEY
node scripts/test-react.mjs
```

预期输出包含:

```text
💭 Thought → ⚡ Action → 👁 Observation → ✅ Answer
链条完整: 是 | 结果正确: 是
```

---

## 八、生产构建流程

```bash
npm run build    # tsc 类型检查 + vite build → dist/
npm run preview  # 本地预览 dist/
```

生产环境需自行配置 Nginx 等反向代理,将 `/api/dashscope` 转发到 `https://dashscope.aliyuncs.com`。

---

## 九、常见问题与排查

| 现象 | 可能原因 | 处理 |
|------|----------|------|
| 提示请先输入 API Key | 未填写 Key | 页面输入或设置 `DASHSCOPE_API_KEY` |
| 请求失败 / 500 | VPN 干扰 TLS | 关闭 VPN 后重试 |
| 右侧无 Thought | 旧版代码 | 确认使用本仓库最新版(含 `thought.ts`) |
| 天气查不到 | 城市名不标准 | 换「深圳」「北京」等常见名 |
| 普通模式也算对了 | 简单题模型可能碰巧对 | 换大数乘法或天气题对比 |

---

## 十、随堂检查

1. ReAct 模式下,为什么要有「第二次」请求大模型?
2. `onStep` 回调的作用是什么?
3. 工具执行结果以什么 role 写回 messages?

<details>
<summary>参考答案</summary>

1. 第一次拿到工具结果后,需要再把结果交给模型,让它组织成自然语言答案。
2. 每完成一步就更新 UI,实现实时展示推理链。
3. `role: "tool"`,并带 `tool_call_id` 与对应调用匹配。

</details>

---

下一篇:[05-课堂演示与自学指南.md](./05-课堂演示与自学指南.md)