8.5 KiB
聊天按需检索与 Markdown 工具
问题与实现
旧聊天只在生成前检索一次,且把全部候选资料直接显示成来源。现在卡片仅在正文出现完整的 [n] 引用后显示,按首次引用顺序排列,保留候选资料的原编号。重复引用不重复显示,代码示例、转义标记和链接不作为引用。候选资料仍保存在消息记录中,重新打开历史对话时按正文重新筛选。
开启知识库检索且 Provider 配置声明 tool_calling 时,请求直接进入模型,模型可先回应,再根据需要调用 rag.search,收到资料后继续输出。首轮不预检索,不等待向量计算。此处是连续的模型轮次,不是在单个厂商 HTTP 响应内部追加上下文。不支持工具调用的 Provider 直接生成并提示本次无法按需检索;关闭知识库检索不会启用此循环。
流程与边界
- 不进行初始检索,直接给模型提供只读检索工具,来源从第一次工具结果开始编号。
- 收集完整工具参数;仅允许执行
rag.search,不执行聊天请求或模型声明的其他工具。 - 补检索继承原查询的过滤条件,仅改变关键词,最多取 6 条,每次超时 30 秒。
- 依据 block_id 去重,新来源追加编号。累计资料正文上限 36,000 字符。
- 把结果作为 tool 消息交给模型继续输出,系统提示明确资料不是指令。
- 最多补检索 3 轮,每轮最多 6 个工具调用;第 4 轮撤除工具,请模型完成回答。继续请求工具时以达到上限结束。
SSE 保持原有事件类型和连续序号。中间模型轮次的 Done 不结束前端连接;ToolCallEnd 延迟到真实检索结束后发送,可携带 status=failed。前端与持久化记录将失败工具映射为 error。各轮输入/输出用量累计,最终发送 Usage。断开连接传播取消,不额外启动脱离请求的检索任务。
后台日志增加 chat.retrieval.completed 与 chat.retrieval.failed,记录轮次和命中数量,不记录查询正文或检索内容。来源卡片表示模型显式引用,不等同于自动验证引用支持该结论。
新增智能体工具
思考模式工具续写兼容
OpenAI-compatible 协议的 Message 增加可选 reasoning_content。聊天保留每轮 ThinkingDelta 并随 assistant 工具调用消息回传;非流式智能体也保留厂商返回的同名字段。历史聊天请求回传已保存的 thinking,普通没有思考内容的消息不附加该字段。
这是 DeepSeek 思考模式工具调用的协议要求:缺少完整思考内容时,后续请求可能返回 HTTP 400。参见 官方说明。模拟 HTTP 回归覆盖两次并行检索后续写,校验实际请求中的思考内容和工具结果 ID,缺少字段时模拟上游返回 400;未使用真实厂商凭据验收。
| 工具 | 功能 | 权限 |
|---|---|---|
| markdown.catalog | 查询格式、警告框别名、渲染限制及编辑流程 | 无文件副作用 |
| markdown.compose | 根据结构化参数生成 Markdown 片段 | 无文件副作用 |
| notes.patch_markdown | 对唯一匹配片段作局部替换 | notes.write,沿用现有确认流程 |
生成支持标题、段落、粗体、斜体、删除线、行内代码、三类列表、引用、警告框、代码块、Mermaid、行内/块公式、链接、图片、表格、分隔线、硬换行、引用链接、HTML 和 YAML 标题/标签元数据。代码围栏按内容增长,避免内容里的反引号提前闭合;表格要求各行列数一致。HTML 最终由现有渲染器净化,不支持执行脚本。数学、图表、警告框仍受用户语法预设控制。
notes.read 新增完整正文 SHA-256 content_hash。局部修改必须提供该版本和唯一的 old_text;版本过期或匹配不唯一时拒绝。保存时在现有 Vault 写锁内再次校验版本,并后台补算向量。元数据标签变化同步到索引标签。生成片段本身不会保存,需调用创建或局部修改工具。标题折叠、撤销、字号等编辑器 UI 状态不伪装成 Markdown 文件操作。
验证方法
思考时间线与消息版本
新消息用 activity 保存思考片段与工具调用 ID 的发生顺序,工具参数和状态继续保存在 tool_calls。界面据此在同一折叠框中穿插显示思考和工具卡片;旧消息缺少事件顺序,只能回退为汇总思考及工具列表,不猜测历史顺序。
AI 消息提供“重新生成”,用户消息提供“编辑”及“保存并重新生成”。每次修改创建同父节点的新消息,原消息和后续回复保留。版本左右切换按钮选择对应分支;后续发送只携带当前分支上下文,不混入其他版本的回复。切换到某版本时恢复其最新后续路径,可在下级回复继续选择旧版本。
数据库追加 parent_message_id、activity_json、active_leaf 和 active_response_id;旧线性历史迁移成单一路径。响应 ID 预留阻止被取消或迟到的旧生成抢占当前分支。新接口 POST /api/chat/conversations/{conversation_id}/messages/{message_id}/select 用于选中版本;ChatRequest 的 retry_message_id 指定编辑或重新生成的原消息,列表响应 versions 给出同级版本 ID。
只读代码块复用编辑器字体偏好和主题代码色。纸间时光 1.9.1 将工作区的三色圆点、底部语言标记和阴影覆盖到聊天 Shiki 代码块;已安装主题需更新。字体大小、代码行号和换行仍由现有偏好控制。
回归:tests/test_chat_versions.py 验证编辑分支、回复再生成、版本切换、活动顺序持久化和迟到回复隔离;前端 ChatView/chat store 测试验证时间线顺序及重试上下文。
- 后端:
pytest tests/test_chat_retrieval.py tests/test_markdown_tools.py tests/test_chat_context.py tests/test_chat_history.py tests/test_agent_core.py -q,使用隔离测试数据目录。 - 前端:
npm test -- src/utils/usedCitations.spec.ts src/features/chat/ChatView.spec.ts,然后npm run build。 - 手动:使用支持工具调用的 Provider,开启检索,提出需要多次查找的问题。确认补检索后继续生成、正文引用出现时才显示卡片,刷新对话后编号不变。模型自行决定是否需要补检索,并非每个问题都必定调用。
- 智能体:允许上述新工具及 notes.read,以格式目录查询 → 生成片段 → 读取笔记 → 局部修改的顺序验证;在读取后人为编辑原笔记,确认过期修改被拒绝。
自动验证使用可控 Provider 流,不调用真实厂商或修改用户笔记。真实模型是否主动检索及引用质量需要单独验收。
聊天渲染与引用格式修正
检索工具向模型仅返回 number、file_path、heading_path、content,内部 citation_id 和定位字段只通过 Citation 事件交给客户端保存。系统提示词要求引用固定使用 [1][2],在对应结论或示例说明旁标注,不重新编号,不把通用知识当作笔记内容。此约束减少格式漂移,不代表自动验证模型结论。
旧回答中的 [cit_blk_…] 按已保存来源 ID 映射为原数字编号,继续显示编号、标题路径与原文摘要卡片。正文数字也可点击定位同一笔记;未知 ID 不产生虚假来源,代码里的标记不视为引用。
工具调用前后的正文用空行分段。聊天代码块显示语言名称与复制源码按钮;Mermaid 支持源码/预览切换与复制。最终 HTML 净化保留 SVG foreignObject 中的标签,同时删除事件处理器,避免图中方框存在但文字消失。
验证方法:运行 test_chat_retrieval.py 检查模型工具结果不包含内部 ID、来源编号稳定及段落边界;运行 usedCitations.spec.ts、markdownRendering.spec.ts、markdownDiagramRendering.spec.ts 检查历史 ID、相邻数字引用、代码排除、语言标签、图中文字净化、源码切换和剪贴板原文。手动复查原有回答的卡片与正文编号均可定位笔记,新建检索问答使用数字编号。
聊天代码块改用包含工具栏与代码内容的统一边框容器。纸间时光 1.9.2 将装饰作用于整个容器,语言名称与复制按钮位于框内,底部保留语言标签。Shiki 行间分隔换行从显示 DOM 中移除,真实空行仍由 .line 保留,复制始终读取独立保存的原文。markdownRendering.spec.ts 覆盖容器、工具栏、空行及原文复制,防止重复行高回归。