Files
agcore/design/prd/2-模板编译器UTF-8编码修复需求.md
T
徐涛 eb7d23de3d docs: 添加模板编译器 UTF-8 编码修复方案与需求文档
- 新增 PDD 方案(编号 32),覆盖 6 处逐字节强转修复、测试设计与版本升级计划
- 新增 PRD 需求(编号 2),定位模板编译阶段中文乱码根因并明确验收标准
- 添加 .codegraph 目录忽略规则,避免本地数据文件入库
2026-08-03 11:21:50 +08:00

173 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# PRD:模板编译器 UTF-8 编码修复
**状态**Draft
**作者**proposal
**日期**2026-08-03
## 1. 核心目标
修复 `agcore::prompt::PromptTemplate` 模板编译器(`src/prompt/template.rs`)对非 ASCII(UTF-8)文本的编码破坏问题:当前 6 处 `bytes[i] as char` 将 UTF-8 字节逐字节强转为 Unicode 码点(等价 Latin-1 解码),导致所有含中文等多字节字符的模板经 `compile → render` 后输出乱码(mojibake),且该乱码会原样发送给 LLM。本次修复使模板编译器对非 ASCII 文本逐字符正确保留,同时保持 ASCII 模板行为完全不变。
## 2. 目标用户与场景
| 用户角色 | 使用场景 | 核心诉求 |
|---------|---------|---------|
| agcore 下游 crate(如 dc-management | 使用 `PromptTemplate` 编译中文 system prompt 模板(validator / collector 采集策略) | 模板文本经编译渲染后与原文一致,不产生乱码 |
| agcore 下游 crate | 使用 `PromptTemplate``#if` / `#each` / `#raw` 块语法,块体内含中文 | 块内容逐字符正确保留 |
| agcore 维护者 | 修复后发布 patch 版本,下游可升级 | 公开 API 不变、ASCII 行为不变、有回归测试保障 |
## 3. 问题描述
### 3.1 现象
dc-management 项目的探索调试日志(`log/explore-20260803-013158915066.txt`)显示:发送给 LLM 的 system 提示词全部乱码:
```
ä½ æ¯éä¾ç¥ç¥ä¸å®¶ï¼è´è´£å®¡æ¥å·²é产åç¼ç ç»æï¼å³ç­ä¸ä¸è½®æç´¢æ¹åã
```
(原文为「你是采集策略专家,负责审查已采集的产品编码结果,决策下一轮搜索方向。」)
日志中的 user 消息与 LLM 响应均为正常中文,证明:乱码发生在**内存字符串**中(模板编译阶段),实际发送给 LLM 的请求体就是损坏文本;日志文件本身(纯 UTF-8 直写)只是如实记录了已损坏的内容。该缺陷为静默失败——模型在容忍乱码的情况下继续工作,但提示词中的规则约束(编码格式、品牌限定等)已被破坏,直接影响下游采集质量。
### 3.2 根因
`src/prompt/template.rs` 共 6 处 `bytes[i] as char`(将 UTF-8 字节逐字节强转为 Unicode 码点,等价 Latin-1 解码):
| 行号 | 位置 | 破坏内容 |
|------|------|---------|
| 255 | `compile_fragments` literal 分支 | 模板纯文本(**主要破坏点**) |
| 275 | `parse_tag` | `{{ 标签 }}` 内部内容(当前为 ASCII 变量名,未触发;若使用中文变量名同样损坏) |
| 324 | `parse_block` else_body | `#if` 分支的 else 块体 |
| 326 | `parse_block` body | `#if` 分支块体 |
| 364 | `parse_each_block` body | `#each` 块体 |
| 389 | `parse_raw_block` content | `#raw` 块内容 |
中文 UTF-8 编码每字 3 字节(emoji 等 4 字节),被拆成多个 Latin-1 字符(如 `0xE4 → ä``0xBD → ½`),产生 mojibake。所有经 `PromptTemplate::compile → render` 链路的非 ASCII 模板文本均被破坏。
### 3.3 影响面
- 所有使用 `PromptTemplate` 且模板含非 ASCII 文本的 agcore 下游项目
- dc-management 的采集策略(validator)与产品信息采集(collector)两条 system prompt 链路全部受影响
- 已确认模板文件本身为合法 UTF-8、日志写出链路无转码、发送链路无转码——问题仅存在于模板编译器
### 3.4 已知线索
- 下游使用 `include_str!` 宏加载模板文件(如 dc-management 的 `src-tauri/src/llm/prompts.rs`),该宏具有编译期 UTF-8 校验:非法编码的模板文件在编译期即报错,故模板文件编码问题不可能存在
- agcore 仓库 `src/prompt/template.rs` 当前没有任何测试(无 `#[cfg(test)]` 模块),本次为模板编译器首次补测试
- 模板语法标签(`{{ }}``#if``#each``#raw`)均为 ASCII,字节比较判断语法是安全的,无需改动
## 4. 功能清单
### v1 必做
- **统一修复 6 处 `bytes[i] as char`**`src/prompt/template.rs` 255 / 275 / 324 / 326 / 364 / 389 行):
- 保持现有 `bytes[i]` 对 ASCII 语法字符(`b'{'` / `b'}'` 等)的字节判断不变
- 字符累积改为按 UTF-8 字符边界推进:从当前位置取完整字符(如 `template[i..].chars().next()`),再按该字符的 UTF-8 长度推进索引(`i += ch.len_utf8()`
- 效果:非 ASCII 文本(中文 / emoji / 任意多字节字符)逐字符正确保留;ASCII 文本行为与修复前完全一致
- **补充回归测试**`src/prompt/template.rs` 新建 `#[cfg(test)] mod tests`):
- 纯中文模板 compile + render 后与原文逐字符一致(主破坏路径 literal)
- 中文文本 + `{{ var }}` 变量插值混合渲染正确
- 中文文本位于 `#if` / `#each` / `#raw` 块体内时正确保留
- emoji 等 4 字节字符正确保留
- 多字节字符紧邻 `{{` / `}}` 标签边界的解析正确性
- 纯 ASCII 模板渲染结果与修复前一致(向后兼容回归)
- 标签内部含非 ASCII 内容(中文变量名如 `{{ 问候 }}`、中文 `#if` 条件)的解析与渲染正确 ← PM Advisor
- **版本号更新**`Cargo.toml` version `0.3.6``0.3.7`
### v1 可选
- 无(上述三项即可完整修复)
### v2 考虑
- 无。CHANGELOG 记录暂不做(本次需求明确暂不记录),后续版本若需要可单独补充
### 非目标
- 不重构模板语法(标签体系、块结构保持现状)
- 不新增模板功能(新标签、新渲染特性)
- 不改动公开 API`PromptTemplate::compile` / `render` 签名与语义不变)
- 不修改其他模块(已扫描 `as char` / `from_utf8_lossy` 等模式,项目中无同类编码风险点)
- 不涉及下游 dc-management 是否回切模板链路的决策(由下游另行评估)
## 5. 边界与假设
| 边界 / 假设 | 来源 | 说明 |
|-------------|------|------|
| ASCII 语法判断可保留 | 语法分析 | 模板标签均为 ASCII(`{``}``#``/``>`),`bytes[i]` 字节比较对 ASCII 安全 |
| 非 ASCII 文本必须逐字符保留 | 需求确认 | 修复目标是编译输出与模板原文一致 |
| ASCII 模板行为零变化 | 需求确认 | 向后兼容是硬约束,需回归测试保障 |
| 公开 API 不变 | 需求确认 | `compile` / `render` 签名不变,下游无需改代码 |
| 修复范围 6 处统一 | 用户确认 | 「尽可能全面的修复」,包括 parse_tag 内的变量名内容 |
| 测试纳入本次范围 | 用户确认 | 用户明确要求补充测试 |
| 版本为 patch0.3.7 | 用户确认 | bug 修复走 patch 版本,尽快让下游升级 |
| CHANGELOG 暂不记录 | 用户确认 | 本次发布不更新 CHANGELOG |
## 6. 术语表
| 术语 | 定义 | 说明 |
|------|------|------|
| mojibake | 乱码 | 文本因编码解码不匹配产生的字符错乱,此处为 UTF-8 字节被按 Latin-1 逐字节解码 |
| Latin-1 | ISO-8859-1 单字节编码 | 0x000xFF 每个字节对应一个 Unicode 码点;`u8 as char` 等价于该解码 |
| `len_utf8()` | Rust 标准库方法 | 返回 `char` 的 UTF-8 编码长度(1–4 字节),用于按字符边界推进索引 |
| `PromptTemplate` | agcore 提示词模板引擎 | 支持变量插值 `{{ var }}`、条件渲染 `#if`、循环 `#each`、原始块 `#raw` |
## 7. 验收标准
- [ ] 中文模板经 `compile` + `render` 后与原文逐字符一致(含纯文本、变量插值混合)
- [ ] 中文位于 `#if` / `#each` / `#raw` 块体内时正确保留
- [ ] emoji 等 4 字节字符正确保留
- [ ] 多字节字符紧邻 `{{` / `}}` 边界时解析正确
- [ ] 纯 ASCII 模板渲染结果与修复前完全一致(以 `src/prompt/composer.rs` 既有 4 个 ASCII 编译测试为基线全部通过,新测试断言等于预定义期望输出)← PM Advisor
- [ ] 6 处逐字节强转全部消除(grep 模式 `as char|char::from` 验证)← PM Advisor
- [ ] `cargo test` 全部通过(含新增测试)
- [ ] `cargo clippy` 无新增警告
- [ ] `Cargo.toml` 版本为 0.3.7
## 8. 风险评估
| 风险 | 影响 | 可能性 | 应对方向 |
|------|------|--------|---------|
| 索引推进改动引入边界回归(模板尾部、空模板、`{{` 未闭合) | 解析错误或 panic | 中 | 新增测试覆盖边界;保持 ASCII 判断逻辑不变 |
| parse_tag 内标签内容的解码方式与 literal 不一致 | 中文变量名场景损坏残留 | 低 | 6 处统一采用同一修复模式 |
| 下游对 0.3.7 的升级未验证中文模板 | 下游继续受影响 | 中 | tag 描述中明确修复内容与最小验证步骤(编译含中文的模板对比输出)← PM Advisor |
| 未来新增模板代码时再次引入逐字节强转 | 同类 bug 复发 | 低 | 回归测试(中文断言)可捕获此类问题 |
## 9. 发布计划
| 阶段 | 范围 | 说明 |
|------|------|------|
| v1 | 6 处修复 + 回归测试 + 版本号 0.3.7 | 本 PRD 范围 |
| 发布 | 打 tag `v0.3.7`,推送 origin | 实际发布动作由维护者执行 |
| 发布说明 | 以 `git tag -a v0.3.7` 的 tag 描述作为发布说明载体(附最小验证:编译含中文的模板并对比输出)← PM Advisor | 不更新 CHANGELOGtag 描述随仓库分发 |
| 下游升级 | dc-management 升级 agcore 引用至 v0.3.7 | 下游自行评估是否回切模板链路 |
## 10. 历史版本
| 版本 | 日期 | 变更说明 |
|------|------|---------|
| v1 | 2026-08-03 | 人工种子(原始) |
### 种子内容
发起方:dc-management 项目排查探索调试日志 `log/explore-20260803-013158915066.txt`,发现发送给 LLM 的 system 提示词乱码。
原始需求描述(一字不改):
「检查一下当前日志 log 目录中保存的日志文件,似乎现在发送给LLM的提示词存在乱码?」
根因:
- `src/prompt/template.rs` 中 6 处 `bytes[i] as char`255 / 275 / 324 / 326 / 364 / 389 行)将 UTF-8 字节逐字节强转为 Unicode 码点(等价 Latin-1 解码),非 ASCII 模板文本被破坏
- 日志文件、发送链路、模板文件编码均无问题,乱码发生在内存字符串(模板编译阶段)
修复方向(用户确认):
- 6 处统一修复:保持 ASCII 语法判断不变,字符累积按 UTF-8 字符边界推进
- 补充回归测试(中文模板恒等、混合插值、块内中文、emoji、边界、ASCII 回归)
- `Cargo.toml` 0.3.6 → 0.3.7,打 tag `v0.3.7`
- CHANGELOG 暂不记录