|
@@ -156,6 +156,26 @@ WARNING 并把冲突键暴露给 `/health` 的 `config_conflicts`。
|
|
|
|
|
|
|
|
**副作用一律放进 lifespan**,模块级只留纯定义。
|
|
**副作用一律放进 lifespan**,模块级只留纯定义。
|
|
|
|
|
|
|
|
|
|
+### 坑 12:工具卡必须按 `tool_call_id` 索引,不能按工具名
|
|
|
|
|
+
|
|
|
|
|
+同一轮里同一个工具会被调多次(实测 agent 会自己换关键词连查 4 次)。
|
|
|
|
|
+只按 `tool` 名索引卡片 → 后一次的结果挂到前一张卡上,界面看起来「结果和
|
|
|
|
|
+参数对不上」。
|
|
|
|
|
+
|
|
|
|
|
+1.5 的契约里没有 `tool_call_id`,2.0 补了它作为**附加字段**(`sse_events`
|
|
|
|
|
+的 tool_call / tool_result)。附加字段安全、改名不安全——见 §四的契约纪律。
|
|
|
|
|
+
|
|
|
|
|
+### 坑 13:`element.textContent += x` 会摧毁子元素
|
|
|
|
|
+
|
|
|
|
|
+`textContent` 的读是「所有后代文本的拼接」、写是「替换全部子节点」。所以
|
|
|
|
|
+`bubble.textContent += token` 这类写法会:① 把子元素(工具卡片)的文本一起
|
|
|
|
|
+读进来,② 赋值时把那些子元素整个删掉。表现是工具卡片被抹成一坨纯文本,
|
|
|
|
|
+而且每次追加都重新拼接全文(O(N²))。
|
|
|
|
|
+
|
|
|
|
|
+正确做法是给正文一个独立的文本节点,并在插入卡片时「断开」当前文本段,
|
|
|
|
|
+以保留时间顺序(见 `static/index.html` 的 `appendToken` / `addCardFor` /
|
|
|
|
|
+`breakSegment`)。
|
|
|
|
|
+
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
## 三、分层与依赖方向
|
|
## 三、分层与依赖方向
|
|
@@ -183,6 +203,8 @@ main.py ──> api/ ──> agents/ ──> tools/
|
|
|
新增开关的默认值不得改变既有行为。
|
|
新增开关的默认值不得改变既有行为。
|
|
|
- **对外契约的字段名只在 `api/sse_events.py` 声明**。其他任何地方都不得
|
|
- **对外契约的字段名只在 `api/sse_events.py` 声明**。其他任何地方都不得
|
|
|
手拼 `data: ` 字符串——字段改名漏掉一处,前端就静默渲染失败。
|
|
手拼 `data: ` 字符串——字段改名漏掉一处,前端就静默渲染失败。
|
|
|
|
|
+- **契约只做加法**:新增字段安全(旧前端忽略未知字段),改名或删除不安全
|
|
|
|
|
+ (静默破坏渲染)。要改已有字段名,先确认 1.5 前端也一起改。
|
|
|
- **函数式注释写「为什么」不写「是什么」**。行内注释只在代码本身无法表达
|
|
- **函数式注释写「为什么」不写「是什么」**。行内注释只在代码本身无法表达
|
|
|
约束时写(顺序要求、副作用、反直觉的取舍)。
|
|
约束时写(顺序要求、副作用、反直觉的取舍)。
|
|
|
- **同步驱动(pymysql / sqlite3)在 async 路由里必须走 `asyncio.to_thread`**,
|
|
- **同步驱动(pymysql / sqlite3)在 async 路由里必须走 `asyncio.to_thread`**,
|