merge: integrate main and reconcile export dependencies
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
# 真实提供商与 MCP 联调压测报告
|
||||
|
||||
> 日期:2026-09-06。代码基线:`cec8daa`,分支 `feat/chat-retrieval-markdown`。环境:Windows、本地 AI Core HTTP 服务、现有 DeepSeek `deepseek-v4-flash`、已注册的 MiniMax Coding Plan MCP。未使用 Mock 替代下面的模型或 MCP 调用。
|
||||
|
||||
## 1. 结论
|
||||
|
||||
普通流式对话、按需检索、聊天创建智能体、内置与 Plugin 工具、MCP 网页搜索、任务读取和经过权限确认的任务修改均完成。任务 API 在独立进程中以 20 并发完成 1,000 个任务的创建、分页、更新和删除,最终无残留,健康检查无错误。
|
||||
|
||||
真实模型部分为小规模并发联调,最高两路并发,不代表厂商吞吐极限。未进行图片理解、上传解析、浏览器渲染、断网重连或长期稳定性压力测试。本报告的历史回读与 Trace 验证通过 HTTP 完成,不等同于浏览器逐项点击验证。
|
||||
|
||||
## 2. 真实对话与工具结果
|
||||
|
||||
| 场景 | 耗时(秒) | 验证结果 |
|
||||
| --- | ---: | --- |
|
||||
| 普通对话,两路并发 | 5.281 / 5.297 | 均收到 ThinkingDelta、TextDelta、Usage、Done,无 Error;每个会话保存 2 条消息 |
|
||||
| 按需知识库检索 | 44.250 | 实际调用 3 次 rag.search;返回 15 条来源;正文包含数字引用 |
|
||||
| 对话创建智能体 | 14.203 | 实际调用 agent.create,返回真实 run_id;对话流结束后继续等待智能体终态 |
|
||||
| 被委托的智能体 | 10.443 | 调用 markdown.catalog 成功,终态 completed;耗时来自 Trace,与对话耗时存在重叠 |
|
||||
| MCP 网页搜索,16,000 Token 预算 | 8.344 | mcp.9ca7ee21603a.web_search 成功,智能体 completed |
|
||||
| 内置与 Plugin 工具,16,000 Token 预算 | 10.391 | chat-policy.plan、math.add、markdown.catalog 均成功,智能体 completed |
|
||||
| 任务只读工具 | 4.312 | tasks.list 成功,智能体 completed |
|
||||
| 任务写入与权限确认 | 5.187 | tasks.update 触发一次确认,allow_once 后指定测试任务变为 done,智能体 completed |
|
||||
|
||||
普通对话首个流事件分别在 4.906 和 4.812 秒到达;此指标不是首个正文字符时间。检索场景首事件为 4.687 秒。
|
||||
|
||||
检索回答保存的来源具有 citation_id、note_id、block_id、file_path、偏移和 number;正文的数字标记与来源记录一起持久化。保存的是候选来源集合,前端仍应按正文引用筛选展示。
|
||||
|
||||
### 2.1 Token 边界结果
|
||||
|
||||
首次将直接创建的两个智能体预算设为 6,000 Token:MCP 搜索与三项内置/Plugin 工具都执行成功,但智能体分别在累计 6,960、6,487 Token 后以 `TOKEN_BUDGET_EXCEEDED` 结束,无最终正文。不能把这两次运行算作完整成功。
|
||||
|
||||
随后以 16,000 Token 重跑,两者均完成,分别使用 6,941、3,477 Token。两次模型规划和输出不同,因此第二次 Token 更少不代表缓存或性能优化。现有预算是累计调用的终止约束,并非能够精准阻止当次请求超出余额;如需要严格费用上限,应继续评估每轮输出额度与输入估算。
|
||||
|
||||
### 2.2 持久化和回放
|
||||
|
||||
回读四个成功运行的 Trace,事件分别为 11、11、15、11 条,序号连续,summary.errors 为 0,终态均为 completed。未重启服务验证中断恢复。
|
||||
|
||||
可在本地 AI 对话页面找到四个以 `[真实压测]` 开头的会话。智能体运行记录保留用于复核;真实任务写入测试仅修改本次创建的唯一任务 ID,结束后该测试任务已删除,未修改原有任务。
|
||||
|
||||
## 3. 任务 HTTP 压力测试
|
||||
|
||||
命令(仓库根目录):
|
||||
|
||||
```powershell
|
||||
backend/.venv/Scripts/python.exe backend/scripts/task-http-stress.py --count 1000 --concurrency 20 --output .local-plans/task-http-live-report.json
|
||||
```
|
||||
|
||||
脚本在独立临时目录中启动 Uvicorn,通过真实回环 HTTP 操作任务,使用真实 SQLite 持久化;不复用用户数据库,也不调用外部模型。
|
||||
|
||||
| 操作 | 次数 | P95(ms) | 最大值(ms) |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| 创建 | 1000 | 145.84 | 189.59 |
|
||||
| 更新 | 1000 | 157.51 | 287.81 |
|
||||
| 删除 | 1000 | 153.48 | 178.98 |
|
||||
| 分页与收尾查询 | 11 | 26.59 | 26.59 |
|
||||
| 健康检查 | 310 | 31.50 | 123.49 |
|
||||
|
||||
总耗时 19,569.35 ms。分页获取的 ID 集合与创建集合一致;更新结果均为 done;最终任务数为 0;健康检查错误数为 0。该耗时不含服务启动。
|
||||
|
||||
首次运行已完成全部任务操作,但在取消健康检查协程的收尾阶段未退出、未写出报告。临时库与操作日志证明业务操作已完成;本次将脚本改为 Event 通知退出,并设置有界等待,重跑成功。未将首次未收尾运行纳入性能统计。
|
||||
|
||||
## 4. 复核方法
|
||||
|
||||
1. 从 `GET /api/providers` 选择现有真实提供商及默认模型,不输出或复制凭据;通过 `GET /api/tools` 和 `/api/mcp/servers` 检查工具注册与服务状态。
|
||||
2. 使用 `POST /api/chat/conversations` 创建带压测前缀的会话,再以 `POST /api/chat` 读取 SSE,统计事件、首事件延迟、错误和工具调用。普通对话关闭 use_rag;检索场景开启 use_rag;委托场景开启 allow_agent。
|
||||
3. 检索提示词为“实际检索 Markdown 警告框,简要说明并用数字引用来源”;委托提示词要求创建只读智能体,调用 markdown.catalog。回读会话消息检查正文和来源字段,检查 agent.create 返回的真实运行终态。
|
||||
4. 使用 `POST /api/agent/runs` 设置明确 allowed_tools、max_steps 和 token_budget。MCP 场景仅允许网页搜索,查询 `Python official documentation` 一次,allow_network 为 true;其他场景不允许网络。
|
||||
5. 任务写入场景先通过 API 创建唯一测试任务,仅允许智能体使用 tasks.update 修改该 ID 的 status 为 done。只对完全匹配此调用的权限票据提交 allow_once;回读任务状态后删除该测试任务。
|
||||
6. `GET /api/agent/runs/{run_id}/trace` 验证事件序号、工具结果、模型调用计数和终态。任务批量正确性使用上面的独立脚本验证。
|
||||
|
||||
本机原始结果保存在 `.local-plans/live-chat-report.json`、`live-agent-retest.json`、`live-trace-report.json`、`live-task-agent-report.json` 和 `task-http-live-report.json`;该目录不提交。附件与委托的定向后端回归另执行 10 项测试,全部通过。
|
||||
|
||||
## 5. 提交与推送状态
|
||||
|
||||
功能改动提交为 `cec8daa`,报告与压测脚本修正提交为 `266608b`。首次推送时 Gitea 返回 `Failed to authenticate user`;完成压测后重试成功,以上提交已推送至 `gitea/feat/chat-retrieval-markdown`。本地 Vault 的既有未提交改动保持原样。
|
||||
@@ -0,0 +1,103 @@
|
||||
# 聊天按需检索与 Markdown 工具
|
||||
|
||||
## 问题与实现
|
||||
|
||||
旧聊天只在生成前检索一次,且把全部候选资料直接显示成来源。现在卡片仅在正文出现完整的 `[n]` 引用后显示,按首次引用顺序排列,保留候选资料的原编号。重复引用不重复显示,代码示例、转义标记和链接不作为引用。候选资料仍保存在消息记录中,重新打开历史对话时按正文重新筛选。
|
||||
|
||||
开启知识库检索且 Provider 配置声明 `tool_calling` 时,请求直接进入模型,模型可先回应,再根据需要调用 `rag.search`,收到资料后继续输出。首轮不预检索,不等待向量计算。此处是连续的模型轮次,不是在单个厂商 HTTP 响应内部追加上下文。不支持工具调用的 Provider 直接生成并提示本次无法按需检索;关闭知识库检索不会启用此循环。
|
||||
|
||||
## 流程与边界
|
||||
|
||||
1. 不进行初始检索,直接给模型提供只读检索工具,来源从第一次工具结果开始编号。
|
||||
2. 收集完整工具参数;仅允许执行 `rag.search`,不执行聊天请求或模型声明的其他工具。
|
||||
3. 补检索继承原查询的过滤条件,仅改变关键词,最多取 6 条,每次超时 30 秒。
|
||||
4. 依据 block_id 去重,新来源追加编号。累计资料正文上限 36,000 字符。
|
||||
5. 把结果作为 tool 消息交给模型继续输出,系统提示明确资料不是指令。
|
||||
6. 最多补检索 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。参见 [官方说明](https://api-docs.deepseek.com/guides/thinking_mode/)。模拟 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` 覆盖容器、工具栏、空行及原文复制,防止重复行高回归。
|
||||
|
||||
## 工作区浮动聊天与智能体委托
|
||||
|
||||
工作区右下角 AI 按钮按需加载非模态浮窗;标题栏可拖动,也可聚焦后用方向键移动,窗口受视口边界限制。关闭仅隐藏,生成与当前会话继续保留。浮窗与 AI 对话页面复用 ChatView 和 chat store,提供新对话、历史选择及跳转到完整页面。会话创建和消息保存仍使用原有本地数据库 API,不建立第二份聊天记录。
|
||||
|
||||
每次发送从编辑器读取当前路径和完整内容,包括未保存修改。`ChatRequest.workspace_context` 包含 `file_path` 和 `content`,单次最多 200 万字符;模型上下文将其明确标记为参考数据。`chat_messages.workspace_context_json` 新增迁移保存快照,历史消息可展开查看。当在完整聊天页面继续时,复用当前会话最后的文件快照;浮窗继续发送则使用最新文件,没有打开文件时不附带旧文件。重新生成和编辑沿用消息分支规则。
|
||||
|
||||
聊天工具栏新增“允许创建智能体”,默认关闭。开启后,支持工具调用的模型可使用 `agent.create` 和 `agent.status`。每次回答最多创建一个运行,沿用同一 Provider/模型、现有 Agent 持久化及权限处理;工具范围固定为笔记读取与修改、Markdown 格式工具及任务管理,不启用网络,限制 10 步、16000 token 和运行时长。`ToolCallEnd.result` 保存运行 ID、状态与结果,历史工具卡片可跳转至智能体页面查看进度、审批或取消。聊天停止不会自动取消已创建的独立智能体;需进入运行页面取消。此阶段创建的是持久化 Agent 运行,不新增人格模板注册体系。
|
||||
|
||||
验证:`test_chat_agents.py` 检查委托开关、一次创建上限、运行预算、无网络及文件参考数据;`test_chat_versions.py` 检查消息快照和运行链接恢复;`chat.spec.ts` 检查连续发送、文件更新、完整页续聊与无文件清除;`WorkspaceChat.spec.ts` 使用真实 Teleport 检查窗口关闭重开时组件不重建、文件内容实时更新和键盘移动。手动验收:打开文件并进行未保存编辑,从右下角发送问题;关闭重开,切换文件再发送;进入 AI 对话页恢复记录。开启智能体后要求创建任务或修改笔记,通过工具卡片进入运行页处理权限确认。
|
||||
|
||||
## 浮窗配置与聊天附件
|
||||
|
||||
浮窗配置区默认折叠,历史选择保留在外部;完整聊天页将配置区与正文和输入框统一到 820px 内容列。窗口右下角手柄支持鼠标拖动与方向键调整尺寸,边界限制在当前视口内。位置和大小保存到本机 localStorage,重置窗口恢复默认大小与位置。
|
||||
|
||||
附件上传复用 `/api/media/attachments`,聊天请求的 `attachments` 最多包含 8 个已上传 ID,消息通过 `attachments_json` 保存并恢复。文档解析在线程中进行:Markdown/TXT 读取 UTF-8,DOCX/PPTX 提取 XML 段落与幻灯片文本,PPT 使用 olefile 读取 PowerPoint 二进制文本记录;扫描页、图像和嵌入对象不等于提取出的文本,需单独上传图片。文档最大 25 MiB,解压总量限制 64 MiB,单份抽取文本最多 20 万字符并显示截断提示。音频沿用现有持久化转写任务及模型路由;支持的录音和视频后缀与音视频页一致,处理失败在聊天中明确反馈。
|
||||
|
||||
图片支持 PNG/JPEG/WebP,最大 20 MiB。优先读取模型列表与 Provider 已声明的 vision 能力,使用当前 Provider/模型原生图片接口提取与本次问题相关的信息。OpenAI Chat Completions、Responses、Anthropic Messages、Ollama 已接入各自图片请求结构。模型清单不提供能力时须在 Provider 中正确声明,不能仅凭模型名称保证支持。启用纯文本上下文检测的模型仍会拒绝无法可靠估算的原生图片请求,此时进入显式配置的降级链。
|
||||
|
||||
图片降级处理器在聊天设置中选择,表示允许将本次会话图片交给该服务;先尝试已注册 MCP,再尝试 Plugin。仅接受明确命名为 image/vision、权限为无或 network.request 的处理器,拒绝策略仍有效。入参适配支持 prompt/query/question、image_source/image_path/path、image_url、attachment_id,其他必填参数交由工具 Schema 校验,不猜测。MCP 超时或失败会继续尝试插件;没有成功处理器则报错。Plugin 使用同一注册接口,后续社区扩展无需改聊天核心。原生视觉提取是一次独立模型调用,真实厂商费用与支持情况需由对应服务验证。
|
||||
|
||||
内置 `chat-operator` Skill 与 `chat-policy` Plugin 随 Host 注册。Plugin 的 `chat-policy.plan` 使用宿主白名单 handler 校验任务与预算,生成读取、执行和核验步骤,不执行任意插件代码。聊天委托创建运行前调用检查;启用的 Skill 加入聊天系统提示词并作为委托运行的 Skill,权限继续由 Agent 管理。用户禁用扩展后不会自动重新启用。
|
||||
|
||||
新增验证:`test_chat_attachments.py` 覆盖 Office/Markdown 文本抽取、旧 PPT 文本记录、截断、音频任务路由、原生视觉优先及 MCP 超时后 Plugin 降级;浮窗测试覆盖尺寸记忆和重置。浏览器实测折叠设置、上传入口和拖动缩放。未使用真实外部模型或 MCP 服务进行付费调用验收。
|
||||
|
||||
### 重试上下文与历史版本一致性(2026-09-07)
|
||||
|
||||
编辑旧用户消息使用该消息的附件;重新生成回答使用该回答版本实际使用的附件和文件快照。旧版未记录回答快照时才回退到它的父用户消息,不读取会话末尾其他轮次的附件。重试不会消耗输入区尚未发送的新附件。
|
||||
|
||||
新回答将 `workspace_context`、`attachments` 和 `context_captured=true` 一起持久化。原用户消息保持不变,因此同一问题的不同回答可以各自恢复生成时的文件内容;工作区浮窗显式传入当前文件时,以本次文件为准。`context_captured=true` 且 `workspace_context=null` 表示该版本明确未附带文件,继续对话或重试时不能回退到原用户消息的旧文件。数据库迁移为既有消息设置 false,保持旧记录可恢复。
|
||||
|
||||
回归覆盖:两轮使用不同附件后编辑/重试第一轮、保留待发送附件、切换工作区文件后生成新版本、历史回读后继续重试、明确清空文件上下文、原版本快照保持不变。
|
||||
Reference in New Issue
Block a user