# 03 · 开发思路与项目架构

本文说明：**为什么这样设计**、**代码怎么分层**、**关键设计决策**。

---

## 学习目标

1. 理解本项目的教学目标与技术选型
2. 能根据目录找到对应源码
3. 理解「思考步骤补全」等健壮性设计

---

## 1. 教学目标（先定目标，再写代码）

| 目标 | 页面/代码如何支撑 |
|------|-------------------|
| 让学生理解 ReAct 概念 | 左侧 `ConceptPanel` 概念面板 |
| 对比「无工具 vs 有工具」 | `ComparePanel` 左右并列 |
| 看见完整推理链 | `ReActChain` 按轮展示 T→A→O→A |
| 工具效果真实可验证 | 接入 Open-Meteo、Frankfurter 等公开 API |
| 课堂只需 DashScope | 模型统一走百炼 `qwen-plus` |

设计原则：**能看得见、能对比、能复现**。

---

## 2. 技术选型

| 层次 | 选择 | 原因 |
|------|------|------|
| 前端框架 | React 18 + TypeScript | 组件化清晰，适合教学演示 UI |
| 构建工具 | Vite 5 | 启动快，内置 dev 代理 |
| 大模型 | DashScope `qwen-plus` | 课程统一要求；支持 Function Calling |
| API 格式 | OpenAI 兼容模式 | `fetch` 即可调用，无需额外 SDK |
| 工具数据 | 公开免费 API | 学生无需再申请天气/汇率 Key |

---

## 3. 项目目录结构

```
06_代码/Day_02/react-demo/
├── docs/                    # 学生学习文档（本目录）
├── scripts/
│   ├── test-tools.mjs       # 工具 API 自测
│   └── test-react.mjs       # ReAct 完整链路自测
├── src/
│   ├── App.tsx              # 主界面：输入、运行、状态管理
│   ├── main.tsx             # React 入口
│   ├── types/index.ts       # 类型定义（Step、Round、Message）
│   ├── data/questions.ts    # 预设示例问题
│   ├── lib/
│   │   ├── dashscope.ts     # 大模型 API 封装（普通模式 + 底层请求）
│   │   ├── react-loop.ts    # ReAct 主循环（核心逻辑）
│   │   ├── thought.ts       # 思考文案生成（健壮性关键）
│   │   ├── rounds.ts        # 步骤分组为「轮次」
│   │   └── tools.ts         # 工具定义 + 真实执行
│   ├── components/
│   │   ├── ConceptPanel.tsx # 概念讲解
│   │   ├── ComparePanel.tsx # 左右对比容器
│   │   ├── ReActChain.tsx   # ReAct 推理链 UI
│   │   └── PlainChain.tsx   # 普通模式流程 UI
│   └── styles/index.css     # 样式
├── vite.config.ts           # 开发代理配置
├── package.json
└── README.md
```

---

## 4. 分层架构图

```mermaid
flowchart TB
    subgraph UI层
        App[App.tsx]
        Compare[ComparePanel]
        ReactUI[ReActChain]
        PlainUI[PlainChain]
    end

    subgraph 业务层
        Loop[react-loop.ts<br/>ReAct 循环]
        Plain[dashscope.ts<br/>plainChat]
        Thought[thought.ts<br/>思考补全]
        Rounds[rounds.ts<br/>轮次分组]
    end

    subgraph 工具层
        Tools[tools.ts<br/>executeTool]
    end

    subgraph 外部服务
        DS[DashScope qwen-plus]
        OM[Open-Meteo]
        FK[Frankfurter]
        Local[本地时间/计算]
    end

    App --> Compare
    Compare --> ReactUI
    Compare --> PlainUI
    App --> Loop
    App --> Plain
    Loop --> Thought
    Loop --> Rounds
    Loop --> Tools
    Loop --> DS
    Plain --> DS
    Tools --> OM
    Tools --> FK
    Tools --> Local
```

---

## 5. 核心模块职责

### 5.1 `dashscope.ts` — 大模型网关

- 封装 `chatCompletion()`：统一发请求、解析错误
- `plainChat()`：普通模式，**不传 tools**
- 开发环境请求走 `/api/dashscope/...`，由 Vite 代理到阿里云

### 5.2 `react-loop.ts` — ReAct 心脏

伪代码逻辑：

```
while 轮次 < 6:
    回复 = 调用模型(messages, tools)
    if 回复没有 tool_calls:
        生成最终 Thought + Answer
        结束
    else:
        生成 Thought（调用前）
        记录 Action（工具名 + 参数）
        执行工具 → 得到 Observation
        把 tool 结果追加到 messages
        继续下一轮
```

### 5.3 `thought.ts` — 为什么需要它？

**问题**：DashScope 在 Function Calling 时，经常**不返回**可见的思考文字（`content` 为空）。

**后果**：页面上只能看到 Action → Observation，学生看不到 Thought。

**解决**：`resolveThought()` 三层策略：

1. 优先用模型返回的 `reasoning_content` 或正文
2. 否则根据「问题 + 工具类型」**自动生成结构化思考文案**
3. 最终回答前再生成「综合 Observation 的思考」

这样课堂上**链条始终完整**。

### 5.4 `tools.ts` — 工具双职责

每个工具包含两部分：

| 部分 | 作用 | 谁使用 |
|------|------|--------|
| `TOOL_DEFINITIONS` | JSON Schema 描述 | 发给大模型，供其选择调用 |
| `executeTool()` | 真正执行逻辑 | 浏览器/脚本在收到 tool_calls 后执行 |

### 5.5 UI 组件分工

| 组件 | 职责 |
|------|------|
| `ReActChain` | 按「第 N 轮」展示 Thought → Action → Observation → Answer |
| `PlainChain` | 展示普通模式「跳过循环、直接输出」 |
| `ComparePanel` | 顶部 VS 摘要 + 左右对比 + 教学提示条 |

---

## 6. 关键设计决策记录

### 决策 1：对比模式默认开启

学生第一眼就能看到「无工具」和「ReAct」的差异，降低讲解成本。

### 决策 2：工具在浏览器端执行

- 模型只负责「决定调什么、传什么参数」
- 真实 HTTP 请求由 `executeTool()` 在浏览器发起
- 天气/汇率 API 支持 CORS，可直接访问

### 决策 3：最多 6 轮推理

防止模型反复调用工具导致死循环或费用失控（`MAX_LOOPS = 6`）。

### 决策 4：API Key 存 sessionStorage

- 方便课堂演示（不用每次输入）
- 关闭标签页即清除
- 不上传到任何自建服务器

### 决策 5：思考步骤可合成

教学演示优先「链条可见」，在模型沉默时由程序补全文案，并标注为「第 N 轮思考」。

---

## 7. 数据流（从用户点击到界面更新）

```mermaid
sequenceDiagram
    participant U as 用户
    participant App as App.tsx
    participant Loop as react-loop
    participant DS as DashScope
    participant Tool as executeTool
    participant UI as ReActChain

    U->>App: 点击「运行演示」
    App->>Loop: runReAct(key, question, onStep)
    Loop->>UI: onStep(input)
    Loop->>DS: chatCompletion + tools
    DS-->>Loop: tool_calls
    Loop->>UI: onStep(thought, action)
    Loop->>Tool: executeTool(name, args)
    Tool-->>Loop: observation 文本
    Loop->>UI: onStep(observation)
    Loop->>DS: messages + tool 结果
    DS-->>Loop: 最终 content
    Loop->>UI: onStep(thought, answer)
    Loop-->>App: RunResult
```

---

## 8. 随堂检查

1. `TOOL_DEFINITIONS` 和 `executeTool` 分别做什么？
2. 为什么单独拆出 `thought.ts`？
3. Vite 代理解决了什么问题？

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

1. 前者描述工具给模型看；后者真正执行工具逻辑。
2. 模型调用工具时常不输出思考文字，需要程序补全以保证教学可见性。
3. 浏览器直接请求 DashScope 会有 CORS 跨域限制，代理转发可绕过。

</details>

---

下一篇：[04-运行流程详解.md](./04-运行流程详解.md)
