# 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 内的变量名内容 | | 测试纳入本次范围 | 用户确认 | 用户明确要求补充测试 | | 版本为 patch(0.3.7) | 用户确认 | bug 修复走 patch 版本,尽快让下游升级 | | CHANGELOG 暂不记录 | 用户确认 | 本次发布不更新 CHANGELOG | ## 6. 术语表 | 术语 | 定义 | 说明 | |------|------|------| | mojibake | 乱码 | 文本因编码解码不匹配产生的字符错乱,此处为 UTF-8 字节被按 Latin-1 逐字节解码 | | Latin-1 | ISO-8859-1 单字节编码 | 0x00–0xFF 每个字节对应一个 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 | 不更新 CHANGELOG,tag 描述随仓库分发 | | 下游升级 | 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 暂不记录