@eoasmxd/freya 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -3
- package/core/package.json +2 -2
- package/doc/_index.md +20 -7
- package/doc/getting-started.md +1 -1
- package/doc/installation-guide.md +2 -2
- package/doc/{architecture-design.md → specifications/architecture-design.md} +7 -4
- package/doc/{config-spec.md → specifications/config-spec.md} +1 -1
- package/doc/{llm-interface-params.md → specifications/llm-interface-params.md} +8 -8
- package/doc/{prompt-system.md → specifications/prompt-system.md} +1 -1
- package/doc/tutorials/_index.md +96 -0
- package/doc/tutorials/part0_basic/0.1_probability_prediction.md +155 -0
- package/doc/tutorials/part0_basic/0.2_attention_and_context.md +145 -0
- package/doc/tutorials/part0_basic/0.3_generation_parameters.md +132 -0
- package/doc/tutorials/part0_basic/0.4_debugging_token.md +99 -0
- package/doc/tutorials/part0_basic/1.1_stateless_and_history.md +143 -0
- package/doc/tutorials/part0_basic/1.2_chat_data_structure.md +179 -0
- package/doc/tutorials/part0_basic/1.3_system_user_assistant.md +147 -0
- package/doc/tutorials/part0_basic/1.4_freya_model_proxy.md +203 -0
- package/doc/tutorials/part0_basic/_index.md +28 -0
- package/doc/tutorials/part1_react/2.1_agency_vs_chatbot.md +127 -0
- package/doc/tutorials/part1_react/2.2_react_mind_model.md +144 -0
- package/doc/tutorials/part1_react/2.3_freya_agent_executor.md +233 -0
- package/doc/tutorials/part1_react/2.4_debugging_loop_deadlock.md +160 -0
- package/doc/tutorials/part1_react/3.1_hardcoded_prompt_pain.md +104 -0
- package/doc/tutorials/part1_react/3.2_decoupled_architecture.md +138 -0
- package/doc/tutorials/part1_react/3.3_freya_dual_read_probe.md +152 -0
- package/doc/tutorials/part1_react/3.4_debugging_composition_placeholder.md +97 -0
- package/doc/tutorials/part1_react/_index.md +28 -0
- package/doc/tutorials/part2_tools/4.1_json_schema_mapping.md +119 -0
- package/doc/tutorials/part2_tools/4.2_tool_call_raw_packet.md +105 -0
- package/doc/tutorials/part2_tools/4.3_freya_tool_execution.md +145 -0
- package/doc/tutorials/part2_tools/4.4_debugging_observation_fix.md +154 -0
- package/doc/tutorials/part2_tools/5.1_observation_injection.md +131 -0
- package/doc/tutorials/part2_tools/5.2_openai_vs_gemini_protocol.md +125 -0
- package/doc/tutorials/part2_tools/5.3_freya_llm_proxy_mapping.md +162 -0
- package/doc/tutorials/part2_tools/5.4_debugging_parallel_call_chaos.md +135 -0
- package/doc/tutorials/part2_tools/_index.md +28 -0
- package/doc/tutorials/part3_memory/6.1_session_state_lifecycle.md +143 -0
- package/doc/tutorials/part3_memory/6.2_physical_sandbox_separation.md +108 -0
- package/doc/tutorials/part3_memory/6.3_freya_session_storage.md +139 -0
- package/doc/tutorials/part3_memory/6.4_debugging_session_concurrency.md +161 -0
- package/doc/tutorials/part3_memory/7.1_context_overflow_loss.md +109 -0
- package/doc/tutorials/part3_memory/7.2_sliding_window_vs_summary.md +85 -0
- package/doc/tutorials/part3_memory/7.3_freya_compactor_impl.md +142 -0
- package/doc/tutorials/part3_memory/7.4_debugging_summarize_deadlock.md +160 -0
- package/doc/tutorials/part3_memory/_index.md +28 -0
- package/doc/tutorials/part4_streaming/8.1_sse_protocol_basics.md +119 -0
- package/doc/tutorials/part4_streaming/8.2_hiding_thoughts_in_stream.md +132 -0
- package/doc/tutorials/part4_streaming/8.3_freya_event_bus.md +104 -0
- package/doc/tutorials/part4_streaming/8.4_debugging_stream_decoder.md +158 -0
- package/doc/tutorials/part4_streaming/9.1_abort_signal_braking.md +162 -0
- package/doc/tutorials/part4_streaming/9.2_async_event_channels.md +142 -0
- package/doc/tutorials/part4_streaming/9.3_freya_abort_billing.md +122 -0
- package/doc/tutorials/part4_streaming/9.4_debugging_abort_lock_deadlock.md +187 -0
- package/doc/tutorials/part4_streaming/_index.md +28 -0
- package/doc/tutorials/part5_plugins/10.1_microkernel_decoupling.md +142 -0
- package/doc/tutorials/part5_plugins/10.2_plugin_metadata_security.md +125 -0
- package/doc/tutorials/part5_plugins/10.3_channel_plugin_development.md +149 -0
- package/doc/tutorials/part5_plugins/10.4_debugging_channel_reconnection.md +160 -0
- package/doc/tutorials/part5_plugins/_index.md +21 -0
- package/doc/tutorials/part6_advanced/11.1_react_model_flaws.md +111 -0
- package/doc/tutorials/part6_advanced/11.2_reflexion_mind_model.md +103 -0
- package/doc/tutorials/part6_advanced/11.3_reflexion_hands_on.md +182 -0
- package/doc/tutorials/part6_advanced/11.4_debugging_reflexion_convergence.md +108 -0
- package/doc/tutorials/part6_advanced/12.1_single_agent_limits.md +100 -0
- package/doc/tutorials/part6_advanced/12.2_multi_agent_patterns.md +121 -0
- package/doc/tutorials/part6_advanced/12.3_freya_multi_agent_routing.md +143 -0
- package/doc/tutorials/part6_advanced/12.4_multi_agent_hands_on.md +176 -0
- package/doc/tutorials/part6_advanced/_index.md +28 -0
- package/doc/tutorials/preface.md +30 -0
- package/package.json +2 -2
- package/plugins/plugin-gemini/package.json +1 -1
- package/plugins/plugin-openai/package.json +1 -1
- package/plugins/plugin-telegram-channel/package.json +1 -1
- package/plugins/plugin-tool-fs/package.json +1 -1
- package/plugins/plugin-tool-memory/package.json +1 -1
- package/plugins/plugin-tool-web/package.json +1 -1
- package/plugins/plugin-wecom-channel/package.json +1 -1
- package/plugins/plugin-weixin-channel/package.json +1 -1
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "5.4 调试与避坑指南:并发多工具调用关联混乱排错"
|
|
3
|
+
weight: 40
|
|
4
|
+
description: "实战调试多工具并发调用下的 call_id 乱序返回与大厂网关协议校验报错,设计底座级时序对齐重组与 Promise 并发容错防御算法。"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 5.4 调试与避坑指南:并发多工具调用关联混乱排错
|
|
8
|
+
|
|
9
|
+
在前几节中,我们对比了解剖了多厂商 Function Calling 的协议差异,并白盒读懂了 plugin-gemini 的协议抹平双向翻译。然而,当我们正式将智能体底座推向高并发、多工具的复杂业务场景时,我们将遭遇多轮工具交互中最凶险的网络大坑 —— **并发工具调用(Parallel Tool Calls)时序紊乱**。
|
|
10
|
+
|
|
11
|
+
当大模型在一轮推理中决定同时调用 3 个工具(如并发查询用户、订单和物流),底座会启动异步多线程/并发调度。
|
|
12
|
+
|
|
13
|
+
然而,网络延迟和接口响应时间是无序的。一旦底座没有对返回的 `Observation`(观察结果)进行物理排序和并发容错保护,就会触发大厂网关的严厉惩罚,或者因为局部接口崩溃导致整个 Agent 彻底挂起。
|
|
14
|
+
|
|
15
|
+
本节我们将实际调试并解决多工具并发乱序与 Promise 局部崩溃这两个致命大坑。
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 一、 并发工具调用的“时序紊乱”与网关报错
|
|
20
|
+
|
|
21
|
+
### 1. 物理时序乱序的成因
|
|
22
|
+
当用户输入“*读取本地 notes.txt 并抓取网页最新发布说明*”时,大模型会输出一个包含两个 Tool Call 意图的消息:
|
|
23
|
+
1. `tool_calls[0]`:调用 `web_fetch(url="https://api.github.com/repos/eoasmxd/freya")`,ID 为 `call_web_001`
|
|
24
|
+
2. `tool_calls[1]`:调用 `read_file(path="notes.txt")`,ID 为 `call_file_002`
|
|
25
|
+
|
|
26
|
+
底座在本地接收到这组意图后,会通过 `Promise.all` 并发启动两个异步任务。
|
|
27
|
+
* 物理事实:`read_file`(本地文件读取)耗时极短,50 毫秒就返回了结果。
|
|
28
|
+
* 物理事实:`web_fetch`(外网请求)因为网络延迟,耗时 1 到 2 秒才返回结果。
|
|
29
|
+
|
|
30
|
+
如果底座的消息接收器写得不够严谨,采用的是“谁先执行完就立刻把谁 push 进历史”的先进先出(FIFO)逻辑。发回给 LLM 的 Message 历史序列会变成:
|
|
31
|
+
1. `assistant` 消息声明意图:先调用 `call_web_001`(网页抓取),再调用 `call_file_002`(本地文件)。
|
|
32
|
+
2. `tool` 消息返回结果:先返回了 `call_file_002`(本地文件结果),再返回了 `call_web_001`(网页抓取结果)。
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
LLM 意图顺序 ────> [1. 网页抓取 (call_web_001)] ──> [2. 本地文件 (call_file_002)]
|
|
36
|
+
│ │
|
|
37
|
+
▼ (网络并发执行) ▼ (本地并发执行)
|
|
38
|
+
底座回传顺序 <──── [2. 网页抓取 (call_web_001)] <── [1. 本地文件 (call_file_002)] (乱序!)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 2. 网关报错与注意力迷惑
|
|
42
|
+
对于严格校验顺序的大厂 API 网关(如 Anthropic Claude 3.5 Sonnet),一旦检测到回传的 `tool` 消息队列的物理顺序,与上一步 `assistant` 中声明的 `tool_calls` 数组顺序不一致,会立刻抛出 `400 BadRequest`。
|
|
43
|
+
即便有些模型不会报错,注意力矩阵在扫描时,如果发现顺序倒置,也极易在接龙时产生张冠李戴的逻辑混乱。
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## 二、 本地调试:复现乱序报错与时序对齐重组算法
|
|
48
|
+
|
|
49
|
+
为了解决这一问题,我们在 Freya 底座的执行器中设计了**时序对齐重组(Re-ordering)算法**。
|
|
50
|
+
|
|
51
|
+
我们通过一段本地调试脚本来展示如果无序会怎样,以及如何用对齐算法重组时序:
|
|
52
|
+
|
|
53
|
+
### 1. 并发乱序与对齐重组调试脚本 (parallel_test.js)
|
|
54
|
+
```javascript
|
|
55
|
+
// 模拟两个异步执行时间不同的真实工具
|
|
56
|
+
const tools = {
|
|
57
|
+
web_fetch: async () => {
|
|
58
|
+
await new Promise(r => setTimeout(r, 1000)); // 慢接口,耗时 1s
|
|
59
|
+
return "GitHub Release v1.0.0...";
|
|
60
|
+
},
|
|
61
|
+
read_file: async () => {
|
|
62
|
+
await new Promise(r => setTimeout(r, 50)); // 快接口,耗时 50ms
|
|
63
|
+
return "本地 notes.txt 记录内容...";
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// 模拟大模型吐出的并发 Tool Calls 声明顺序
|
|
68
|
+
const originalToolCalls = [
|
|
69
|
+
{ id: "call_web_001", name: "web_fetch" },
|
|
70
|
+
{ id: "call_file_002", name: "read_file" }
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
async function runParallelToolCalls() {
|
|
74
|
+
console.log("🚀 开始并发执行工具...");
|
|
75
|
+
|
|
76
|
+
// 1. 启动并发 Promise 链
|
|
77
|
+
const toolPromises = originalToolCalls.map(async (tc) => {
|
|
78
|
+
const result = await tools[tc.name]();
|
|
79
|
+
// 💡 物理记录:每个 Promise 执行完后,返回包含其 ID 的完整结果对象
|
|
80
|
+
return {
|
|
81
|
+
role: "tool",
|
|
82
|
+
tool_call_id: tc.id,
|
|
83
|
+
content: result,
|
|
84
|
+
toolName: tc.name
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// 2. 无序接收 (Promise.all 出来后的顺序是 map 的初始顺序,但在底层复杂执行时容易因为推送错乱)
|
|
89
|
+
const rawResults = await Promise.all(toolPromises);
|
|
90
|
+
|
|
91
|
+
// 3. ⚙️ 时序对齐重组算法 (Re-ordering)
|
|
92
|
+
// 必须严格根据大模型原始声明的 originalToolCalls 顺序,对 rawResults 进行重排!
|
|
93
|
+
const alignedResults = originalToolCalls.map((originalCall) => {
|
|
94
|
+
return rawResults.find((res) => res.tool_call_id === originalCall.id);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
console.log("\n=== 原始大模型意图声明顺序 ===");
|
|
98
|
+
console.log(originalToolCalls.map(c => c.id));
|
|
99
|
+
|
|
100
|
+
console.log("\n=== ⚙️ 经过底座重组对齐后的回传顺序 ===");
|
|
101
|
+
console.log(alignedResults.map(r => r.tool_call_id));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
runParallelToolCalls();
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
通过这一层 `alignedResults` 的重映射重组,底座强行保证了回传的 Observation 消息队列与大模型意图数组的**物理相对位置 100% 对齐**,彻底规避了乱序返回引发的 400 校验风暴。
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## 三、 防御实战:局部工具执行崩溃的整体防御 (Promise Isolation)
|
|
112
|
+
|
|
113
|
+
在并发执行 3 个工具(A, B, C)时,还有一个极其经典的工程大坑:**如果其中一个工具执行报错(或者网络崩溃),会导致底座整体决策挂起。**
|
|
114
|
+
|
|
115
|
+
如果在底座中你直接写成:
|
|
116
|
+
```typescript
|
|
117
|
+
// ❌ 脆弱的并发控制
|
|
118
|
+
const results = await Promise.all(toolPromises);
|
|
119
|
+
```
|
|
120
|
+
如果工具 B 抛出了异常:
|
|
121
|
+
* `Promise.all` 拥有 **“快速失败(Fast-Fail)”** 的物理特性。只要有一个 Promise 被 reject,整个 `Promise.all` 会立刻抛出异常中断执行。
|
|
122
|
+
* 这会导致原本运行成功的工具 A 和 C 的执行结果(Observation)**被直接丢弃**。
|
|
123
|
+
* 大模型在下一次推理时,完全收不到 A 和 C 的结果,整轮 Agent 交互宣告流产。
|
|
124
|
+
|
|
125
|
+
### 🌟 黄金防御规范:Promise 隔离与安全 Resolve
|
|
126
|
+
为了保障智能体底座的强鲁棒性,底座在并发调度 Promise 链时,必须确保**每个子 Promise 在物理上是独立隔离的,绝不能把 Error 抛到外层**:
|
|
127
|
+
|
|
128
|
+
1. **子链 Catch 隔离**:在 `toolCall.map` 内部的 async 块中,必须使用 `try-catch` 包裹住具体工具的 `tool.execute()`。
|
|
129
|
+
2. **错误 Observation 包装**:一旦工具捕获到 Error,**绝对不能 reject**。必须将其 catch 转化为一个合法的 Observation 对象返回:
|
|
130
|
+
`return { role: "tool", content: "Error during execution: ..." }`。
|
|
131
|
+
3. 这保证了 `Promise.all` 接收到的全部都是 resolved 状态的合法对象。即使某个接口彻底瘫痪,底座也能带着这个接口的报错 Observation 与另外两个工具的成功结果,整体平稳回流给大模型,供其在下一轮进行得体的容错决策。
|
|
132
|
+
|
|
133
|
+
通过本节的并发乱序对齐重组与 Promise 隔离防御,我们彻底理清了多轮 Function Calling 在高并发、多通道环境下的物理稳定壁垒。
|
|
134
|
+
|
|
135
|
+
在下一部分中,我们将暂时告别喧嚣的“工具执行”,推开智能体大脑的静谧之门,去探秘记忆(Session)在底座中存储、压缩与淘汰的奥秘。
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "第二部分:掌控“工具”"
|
|
3
|
+
weight: 30
|
|
4
|
+
bookCollapseSection: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 第二部分:连接物理世界 —— 智能体如何掌控“工具” (Tool Call)
|
|
8
|
+
|
|
9
|
+
本部分将深入探讨大模型是如何理解并调用外部物理世界的 API 工具的。我们将解剖 JSON Schema 参数描述符、网络 Raw 数据包、以及跨厂商协议流派(OpenAI/Gemini)的抹平架构。
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 🧭 章节导学与阅读清单
|
|
14
|
+
|
|
15
|
+
### 🛠️ 第 4 章:大模型感知与选择工具的底层技术本质
|
|
16
|
+
剖析工具描述符的 JSON Schema 规范、捕获并拆解大模型返回的 Tool Call 原始数据,并探索工具执行的安全沙箱设计。
|
|
17
|
+
* 👉 **[4.1 语言到动作的转换](4.1_json_schema_mapping.md)**
|
|
18
|
+
* 👉 **[4.2 【白盒剖析】Tool Call Raw 数据包结构](4.2_tool_call_raw_packet.md)**
|
|
19
|
+
* 👉 **[4.3 【白盒剖析】工具安全防线与本地执行](4.3_freya_tool_execution.md)**
|
|
20
|
+
* 👉 **[4.4 调试与避坑指南:错误 Observation 与自我修正](4.4_debugging_observation_fix.md)**
|
|
21
|
+
|
|
22
|
+
### 🔌 第 5 章:大厂 Function Calling 协议差异与多轮关联流派
|
|
23
|
+
分析 OpenAI(基于单会话多 `call_id` 关联)与 Gemini 在多轮工具调用下的协议设计哲学,并拆解底座代理层的解耦与统一抹平设计。
|
|
24
|
+
* 👉 **[5.1 工具执行结果的归流](5.1_observation_injection.md)**
|
|
25
|
+
* 👉 **[5.2 两大厂商协议流派交锋](5.2_openai_vs_gemini_protocol.md)**
|
|
26
|
+
* 👉 **[5.3 【白盒剖析】Freya 大模型代理层协议映射](5.3_freya_llm_proxy_mapping.md)**
|
|
27
|
+
* 👉 **[5.4 调试与避坑指南:并发多工具调用关联混乱排错](5.4_debugging_parallel_call_chaos.md)**
|
|
28
|
+
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "6.1 会话管理生命周期与数据结构"
|
|
3
|
+
weight: 10
|
|
4
|
+
description: "探秘智能体会话(Session)的物理生命周期与状态寄存器,剖析海量历史会话下的轻重双层索引与懒加载防爆内存机制。"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 6.1 会话管理生命周期与数据结构
|
|
8
|
+
|
|
9
|
+
在第一和第二部分中,我们打通了智能体(Agent)的决策死循环与工具执行链路。然而,大模型本身依然是无状态的(Stateless)。要让智能体在经历漫长的时间线、多次网络重启,甚至服务器断电后,依然能连贯地承接上文的语境并继续工作,底座必须建立起一套高内聚的**会话(Session)与状态维护机制**。
|
|
10
|
+
|
|
11
|
+
对智能体而言,会话不是简单的“聊天历史文本备份”,而是它大脑的**心流状态寄存器**。
|
|
12
|
+
|
|
13
|
+
本节我们将详细拆解会话在底座中的物理生命周期,解剖 Session 的核心状态字段,并针对海量多会话长期运行环境下极易触发的“内存泄露与 OOM 崩溃”大坑提供架构级的防御策略。
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 一、 记忆是智能体“心流”的物理载体
|
|
18
|
+
|
|
19
|
+
如果把 ReAct 执行器比作智能体的“CPU”,那么 Session 会话就是它的“内存条”和“高速缓存”。
|
|
20
|
+
|
|
21
|
+
智能体的会话管理之所以比普通 Web 应用的 Session 复杂得多,是因为它不仅要保存“历史对话文本”,还要实时跟踪和更新智能体当前的**心智与物理状态空间**。这包括:
|
|
22
|
+
* **当前激活的技能卡(Active Skill ID)**:智能体当前正处于什么专业的工作模式中?
|
|
23
|
+
* **当前激活的工具箱(Active Toolbox IDs)**:大模型当前有权支配哪套工具资源?
|
|
24
|
+
* **工具箱闲置轮数(Toolbox Idle Rounds)**:哪些工具箱正处于闲置状态,需要底座主动进行垃圾回收?
|
|
25
|
+
* **当前绑定的基模型(Model ID)**:本次对话大模型降级链当前落在了哪一家提供商的哪一个模型上?
|
|
26
|
+
|
|
27
|
+
所有这些非文本的结构化状态,共同缝合出了智能体在进程运行时层面的**“心流(Context Flow)”**。
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 二、 会话(Session)的物理生命周期
|
|
32
|
+
|
|
33
|
+
在 Freya 底座的设计中,会话在物理上经历四个核心生命周期阶段:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
[创建 (Birth)] ──> 分配 Session ID,载入默认模型与工具箱
|
|
37
|
+
│
|
|
38
|
+
▼
|
|
39
|
+
[流转与追加 (Ingestion)] ──> 每轮 ReAct 往返更新内存缓存,写透落盘
|
|
40
|
+
│
|
|
41
|
+
▼
|
|
42
|
+
[压缩与重组 (Compacting)] ──> 逼近 Token 额度上限,调用 LLM 摘要归档
|
|
43
|
+
│
|
|
44
|
+
▼
|
|
45
|
+
[冷冻与消亡 (GC / Death)] ──> 长期闲置,内存自动淘汰释放,只留磁盘物理文件
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 1. 创建(Birth)
|
|
49
|
+
当用户通过 Telegram 或 CLI 连入智能体,底座首先会为其分配一个全局唯一的 `sessionId`。
|
|
50
|
+
执行器会为其初始化一个空白的状态结构,注入默认的模型路由配置、默认元工具箱(`meta`),并写入物理磁盘。
|
|
51
|
+
|
|
52
|
+
### 2. 流转与追加(Ingestion & Update)
|
|
53
|
+
在 `while(loop)` 决策死循环中,每一次大模型输出 Tool Call 或是执行完工具得到 Observation,执行器都会调用 `SessionManager.appendMessage()` 往历史列表里追加数据。
|
|
54
|
+
为了防止系统宕机导致记忆丢失,每一次内存中的追加,底座都**必须执行“写透(Write-Through)”策略** —— 实时同步写入磁盘的持久化文件中。
|
|
55
|
+
|
|
56
|
+
### 3. 压缩与重组(Compacting & Archive)
|
|
57
|
+
当多轮对话的 Token 消耗逼近大模型的最大窗口(Context Window)时,底座会主动切断正常循环,启动 `SessionCompactor`,将陈旧的对话记录编译为“摘要记忆(Summary)”,替换原本琐碎的 messages。
|
|
58
|
+
|
|
59
|
+
### 4. 冷冻与消亡(GC / Death)
|
|
60
|
+
在长期运行环境中,数以千计的会话如果一直将完整消息列表常驻在服务器的内存 Map 中,会迅速耗尽 Node.js 的堆显存。因此,当会话处于非活跃状态时,底座通过索引分离机制**只保留轻量索引在内存中**,完整消息历史只留存在磁盘上的物理文件。当下一次该会话被唤醒交互时,底座再从磁盘按需延迟加载。
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## 三、 Session 数据结构的物理定义
|
|
65
|
+
|
|
66
|
+
在 Freya 底座中(位于 `packages/core/src/session/types.ts`),会话采用**“轻量索引与完整实体分层”**的设计:
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
// 1. 存入 sessions.json 的全局轻量索引条目
|
|
70
|
+
export interface SessionIndex {
|
|
71
|
+
id: string; // 会话业务标识(如 'main')
|
|
72
|
+
uuid: string; // 物理存储唯一 UUID
|
|
73
|
+
parentId: string | null; // 父会话指针(支持子智能体分支)
|
|
74
|
+
archived: boolean; // 是否已归档
|
|
75
|
+
archivedAt: string | null; // 归档时间戳
|
|
76
|
+
summary?: string; // 压缩后的长期记忆摘要
|
|
77
|
+
updatedAt: string; // ISO 8601 最后更新时间
|
|
78
|
+
modelId?: string; // 当前绑定的模型 ID
|
|
79
|
+
providerId?: string; // 当前绑定的提供商 ID
|
|
80
|
+
activeSkillId?: string; // 当前激活的技能卡
|
|
81
|
+
activeToolboxIds?: string[]; // 当前激活授权的工具箱列表
|
|
82
|
+
toolboxIdleRounds?: Record<string, number>;// 工具箱闲置迭代计数器
|
|
83
|
+
// Token 与计费统计
|
|
84
|
+
promptTokens?: number;
|
|
85
|
+
completionTokens?: number;
|
|
86
|
+
totalTokens?: number;
|
|
87
|
+
cost?: number;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 2. 内存中的完整会话对象(继承轻量索引,挂载历史消息实体)
|
|
91
|
+
export interface Session extends SessionIndex {
|
|
92
|
+
history: LLMMessage[]; // 完整的对话消息列表
|
|
93
|
+
lastSnapshotId: string | null; // 最新压缩快照文件 ID
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## 四、 【架构防线】海量多会话下的内存防爆设计:轻重双层索引与懒加载
|
|
100
|
+
|
|
101
|
+
很多新手开发者在设计智能体会话系统时,习惯将所有会话的完整对话历史全部常驻在全局 Map 中。
|
|
102
|
+
|
|
103
|
+
### 1. 内存泄露与 OOM 隐患
|
|
104
|
+
大模型的 Message 历史列表由于包含了大量 raw JSON、图片 Base64 附件和庞大的工具返回文本,**单个 Session 的内存占用可能高达数兆至数十兆字节**。
|
|
105
|
+
在长期运行或频繁创建分支会话的场景下,如果有数千个历史会话积累,常驻内存的体积会以惊人的速度膨胀,极易突破 Node.js 默认 1.4 GB 的堆内存上限,引发 **OutOfMemory (OOM) 崩溃**。
|
|
106
|
+
|
|
107
|
+
### 2. Freya 的物理防爆防线:双层索引与按需懒加载(Lazy-Loading)
|
|
108
|
+
为了彻底规避 OOM,Freya 在 `SessionManager` 与 `persistence.ts` 中设计了**“轻重分离的双层索引”**架构:
|
|
109
|
+
|
|
110
|
+
* **轻量全局索引常驻(`sessions.json`)**:
|
|
111
|
+
系统启动时,`persistence.loadIndex()` 仅将几百字节的 `SessionIndex` 载入内存中的 `sessionIndices` Map。即使存在 100,000 个历史会话,内存开销也仅有几兆字节。
|
|
112
|
+
* **消息体按需懒加载(`lazyLoadSession`)**:
|
|
113
|
+
只有当某个特定会话收到用户消息或需要与 LLM 交互时,底座才通过 `lazyLoadSession(id)` 从 `~/.freya/data/sessions/<uuid>/session.json` 中异步读取 `history` 消息体并反序列化载入内存。
|
|
114
|
+
* **消息压缩快照外置(`<snapId>.json`)**:
|
|
115
|
+
当活跃会话发生上下文压缩时,被裁剪掉的历史消息会被存盘为独立的快照文件,不阻塞主会话文件与内存体积。
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
// 摘自 packages/core/src/session/session-manager.ts
|
|
119
|
+
private async lazyLoadSession(id: string): Promise<Session> {
|
|
120
|
+
// 1. 如果内存已有且未归档,极速返回缓存
|
|
121
|
+
const cached = this.sessions.get(id);
|
|
122
|
+
if (cached && !cached.archived) return cached;
|
|
123
|
+
|
|
124
|
+
// 2. 从轻量索引中检索元数据
|
|
125
|
+
const idx = this.findLatestIndexById(id);
|
|
126
|
+
if (!idx) throw new Error(`会话不存在: ${id}`);
|
|
127
|
+
|
|
128
|
+
// 3. 仅在命中调用时,从磁盘物理文件按需反序列化完整消息历史
|
|
129
|
+
const data = await this.persistence.loadSessionData(idx.uuid);
|
|
130
|
+
const session: Session = {
|
|
131
|
+
...idx,
|
|
132
|
+
history: data.history,
|
|
133
|
+
lastSnapshotId: data.lastSnapshotId
|
|
134
|
+
};
|
|
135
|
+
this.sessions.set(id, session);
|
|
136
|
+
return session;
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
通过这一层双层索引与按需懒加载屏障,智能体底座的内存开销与总历史会话量彻底解耦,无论历史沉淀了多少会话,系统都能始终平稳运行。
|
|
141
|
+
|
|
142
|
+
本节我们解剖了智能体会话的物理生命周期、核心状态字段与双层索引懒加载内存防爆设计。在下一小节中,我们将重点分析“数据与源码的物理隔离架构”,看懂 Freya 到底是如何在操作系统底层安全划定边界的。
|
|
143
|
+
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "6.2 【沙箱设计】数据与源码物理隔离"
|
|
3
|
+
weight: 20
|
|
4
|
+
description: "阐述数据(Data)、配置(Config)与源码(Code)物理隔离的安全性必要性,剖析 Freya 运行时沙箱目录划分机制。"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 6.2 【沙箱设计】数据与源码物理隔离
|
|
8
|
+
|
|
9
|
+
在构建玩具级的智能体 Demo 时,最省事的做法就是直接在项目根目录下创建一个 `data/` 文件夹,然后把所有的会话(Session)JSON 文件直接往里丢。
|
|
10
|
+
|
|
11
|
+
然而,一旦我们将智能体底座推向正式的商业化部署或容器化(Docker)云原生架构时,这种“混在一起住”的设计会立刻撞上物理红线。它轻则导致每次代码更新时用户记忆全丢,重则直接引发核心密钥外泄的网络安全事故。
|
|
12
|
+
|
|
13
|
+
本节我们将以**软件架构师的视角**,深度拆解“数据(Data)、配置(Config)与源码(Code)”物理隔离的架构原则,并解密 Freya 的运行时沙箱隔离目录划分设计。
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 一、 数据与源码混合存储的四大物理灾难
|
|
18
|
+
|
|
19
|
+
在软件工程中,**只读代码(Static Code)**与**动态状态(Dynamic State)**拥有完全不同的生命周期和安全性等级。如果将它们存放在同一个物理目录下,将引发以下四个大坑:
|
|
20
|
+
|
|
21
|
+
### 1. 密钥与敏感数据外泄的 Git 灾难
|
|
22
|
+
* **物理隐患**:如果你的 Agent 会话中包含了用户的机密对话或临时 API Key,且它们被写在源码库下的某个文件夹内。
|
|
23
|
+
* **后果**:开发人员在终端执行 `git commit -a` 提交代码并推送至开源 GitHub 仓库时,极易由于忽略了 `.gitignore` 的配置,把整套敏感的会话数据库和全局密钥物理暴露在公网上,造成无可挽回的机密泄漏事故。
|
|
24
|
+
|
|
25
|
+
### 2. 容器化部署下的 `EROFS`(只读文件系统)崩溃
|
|
26
|
+
在现代云原生架构(Kubernetes / Docker)中,推行的是**不可变基础设施(Immutable Infrastructure)**原则。
|
|
27
|
+
* **物理限制**:构建出来的容器镜像是**绝对静态、物理只读**的。
|
|
28
|
+
* **后果**:当容器被拉起并运行时,如果执行器试图往内置的源码路径(如 `packages/core/dist/data/`)下写入 Session 状态文件,操作系统底层会直接抛出 `Error: EROFS: read-only file system` 写入拒绝异常,导致智能体进程瞬间挂起瘫痪。
|
|
29
|
+
|
|
30
|
+
### 3. 滚动更新导致的用户记忆“全员灰飞烟灭”
|
|
31
|
+
* **物理事实**:容器的生命周期是短暂的。每次我们修复源码 Bug 并进行滚动更新时,旧的容器实例会被直接销毁(连同它内部的所有本地文件),新容器重新启动。
|
|
32
|
+
* **后果**:如果你把用户记忆写在只读源码文件夹旁边,每次发版,用户的所有多轮会话记忆就会被彻底抹去,给终端用户带来灾难性的“脑残”体验。
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## 二、 Freya 的运行时物理沙箱隔离划分
|
|
37
|
+
|
|
38
|
+
为了彻底解决上述痛点,Freya 架构引入了**物理沙箱隔离设计**。
|
|
39
|
+
|
|
40
|
+
系统强制规定:**以运行环境的用户主目录为基准,在物理操作系统的底层划定清晰的四大物理隔离边界:**
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
操作系统物理文件系统 (OS File System)
|
|
44
|
+
├── 源码仓库目录 (Git Code Base,只读)
|
|
45
|
+
│ └── Packages/ (core, sdk, plugins)
|
|
46
|
+
│
|
|
47
|
+
└── 用户主目录 ~/.freya/ (运行时持久化沙箱,可读写)
|
|
48
|
+
├── config/ --> 用户个性化配置 (freya.json) 与 Prompt 覆盖覆盖
|
|
49
|
+
├── data/ --> 运行时历史记忆 (sessions/) 与长期记忆 (memories.json)
|
|
50
|
+
└── workspace/ --> 宿主与大模型交互隔离的读写文件沙箱 (download/)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 1. 源码层(Code Base / 只读)
|
|
54
|
+
存放编译后的 TS/JS 静态文件、系统内置的元配置文件(只读,无法被运行时修改)。它完全处于 Git 的版本控制下。
|
|
55
|
+
|
|
56
|
+
### 2. 运行时配置层(Config Base / 可读写)
|
|
57
|
+
默认位于 `~/.freya/config/`。存放用户部署后自定义的 `freya.json` 授权、大模型网关密钥等。该目录支持热重载,但绝不应存入 Git 仓库。
|
|
58
|
+
|
|
59
|
+
### 3. 持久化数据层(Data Base / 可读写)
|
|
60
|
+
默认位于 `~/.freya/data/`。专门用于存放多轮会话历史 `sessions/`、长期记忆向量数据 `memories/` 等。此目录是智能体大脑的物理保险库,**可以被独立挂载(Volume Mount)到外部云盘**,实现数据的滚动冷备份与高可用。
|
|
61
|
+
|
|
62
|
+
### 4. 读写隔离沙箱区(Workspace Sandbox / 限制读写)
|
|
63
|
+
默认位于 `~/.freya/workspace/`。这是为外部插件(如文件处理插件、代码执行插件)专门划定的运行沙箱。插件进行任何文件读写、临时文件下载(如大模型返回的图片),必须被强行限定在此工作区之内,**绝对禁止**越界读写服务器的其他敏感文件(如操作系统的 `/etc/passwd`)。
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## 三、 沙箱物理目录的初始化与定位机制
|
|
68
|
+
|
|
69
|
+
为了让这一套沙箱架构在开发环境和云端生产环境都能“开箱即用”,底座必须在启动时能够动态定位并自初始化这一套目录结构。
|
|
70
|
+
|
|
71
|
+
### 1. 动态定位机制
|
|
72
|
+
在 Freya 源码 `packages/core/src/utils/paths.ts` 中,底座通过检测环境变量 `FREYA_HOME` 或用户系统 Home 目录,定义了运行态根目录 `PROJECT_ROOT` 以及程序代码物理安装根目录 `APP_ROOT`:
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
const customHome = process.env.FREYA_HOME;
|
|
76
|
+
|
|
77
|
+
/** 运行态下的项目根目录(数据与配置存放区) */
|
|
78
|
+
export const PROJECT_ROOT = customHome
|
|
79
|
+
? path.resolve(customHome)
|
|
80
|
+
: path.join(os.homedir(), '.freya');
|
|
81
|
+
|
|
82
|
+
/** 程序代码物理安装根目录(只读代码与包内默认资源区) */
|
|
83
|
+
export const APP_ROOT = resolveAppRoot();
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 2. 各模块按需自检初始化 (On-Demand Initialization)
|
|
87
|
+
与采用全局单体函数统一创建所有目录的方式不同,为了实现精细的职责分工与防错,Freya 底座采用**各功能模块在其自身的读写逻辑中按需自检并自动初始化**(On-Demand Creation)的模式。
|
|
88
|
+
|
|
89
|
+
例如,在负责多轮会话持久化存储的 `packages/core/src/session/persistence.ts` 模块中:
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
const sessionDirByUuid = (uuid: string) => path.join(PROJECT_ROOT, 'data', 'sessions', uuid);
|
|
93
|
+
|
|
94
|
+
// 在写入会话时,若相应目录不存在,则在底层自动按需创建
|
|
95
|
+
await fs.mkdir(sessionDirByUuid(uuid), { recursive: true });
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
同样地,在负责动态技能扫描的 `packages/core/src/skill/skill-registry.ts` 模块中:
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
const runtimeSkillsDir = path.join(PROJECT_ROOT, 'skills');
|
|
102
|
+
// 自检并按需自动创建运行时用户技能卡目录
|
|
103
|
+
await fs.mkdir(runtimeSkillsDir, { recursive: true });
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
通过这一巧妙的 Bootstrap 自初始化设计,我们在物理操作系统的底层成功为智能体圈定了安全、隔离的自留地。
|
|
107
|
+
|
|
108
|
+
本节我们从系统架构和网络安全高度,拆解了数据与源码物理隔离设计的成因与实现。在下一小节中,我们将实际进入 Freya 源码,去解刨负责将 Session 进行读写落盘的具体实现者 —— `SessionManager`。
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "6.3 【白盒剖析】物理持久化存储机制"
|
|
3
|
+
weight: 30
|
|
4
|
+
description: "白盒解剖 Freya 的 session-manager.ts 源码,拆解索引与数据分离(延迟加载)设计与并发队列锁的物理实现。"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 6.3 【白盒剖析】物理持久化存储机制
|
|
8
|
+
|
|
9
|
+
在 6.2 节中,我们确立了将持久化数据剥离至 `~/.freya/data/` 目录的沙箱隔离架构。本节我们将正式进入负责这一记忆读写与落盘的物理实现者 —— **会话管理器**(位于 `packages/core/src/session/session-manager.ts`)。
|
|
10
|
+
|
|
11
|
+
我们将一起读懂 `FreyaSessionManager` 是如何通过**索引与数据分离设计**优化 I/O 吞吐、如何通过**并发异步排队锁(Enqueue Write Lock)**物理防御高并发下的文件覆写冲突,以及如何调度后置异步压缩机制的。
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 一、 优化吞吐:索引与数据分离(延迟加载)
|
|
16
|
+
|
|
17
|
+
在大规模在线智能体系统中,当用户打开聊天页面时,前端需要拉取最近的会话列表,展示每个会话的标题、最后修改时间以及计费消耗。
|
|
18
|
+
|
|
19
|
+
如果底座的设计是“只要读取会话,就必须连同它过去的 100 条多轮历史消息一起读入内存”,高并发下服务器的磁盘 I/O 读写带宽会瞬间被撑爆。
|
|
20
|
+
|
|
21
|
+
为此,Freya 引入了**索引与数据分离(Index & Data Separation)**的优化架构:
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
export class FreyaSessionManager {
|
|
25
|
+
private persistence = new FreyaSessionPersistence();
|
|
26
|
+
|
|
27
|
+
// 1. 全量会话详细数据缓存 Map (包含庞大的 history 数组,按需加载)
|
|
28
|
+
private sessions = new Map<string, Session>();
|
|
29
|
+
|
|
30
|
+
// 2. 超轻量级元数据索引缓存 Map (只存放 UUID、修改时间、激活状态,无 history)
|
|
31
|
+
private sessionIndices = new Map<string, SessionIndex>();
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### 1. 延迟加载 (Lazy Loading) 物理逻辑
|
|
36
|
+
当内核执行器调用 `getOrCreate(id)` 时,底座首先扫描内存中的 `sessionIndices` 索引。如果内存 Map 未命中所请求的详细数据,才被动触发 `lazyLoadSession` 从磁盘中拉取:
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
private async lazyLoadSession(id: string): Promise<Session> {
|
|
40
|
+
const cached = this.sessions.get(id);
|
|
41
|
+
if (cached && !cached.archived) return cached; // 命中缓存,直接返回
|
|
42
|
+
|
|
43
|
+
const idx = this.findLatestIndexById(id);
|
|
44
|
+
if (!idx) {
|
|
45
|
+
throw new Error(`会话不存在: ${id}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 💡 JIT (Just-In-Time) 磁盘加载:仅在需要进行多轮推理时,才物理读取 history 详情
|
|
49
|
+
const data = await this.persistence.loadSessionData(idx.uuid);
|
|
50
|
+
const session: Session = {
|
|
51
|
+
id: idx.id,
|
|
52
|
+
uuid: idx.uuid,
|
|
53
|
+
history: data.history, // 读入历史消息
|
|
54
|
+
...
|
|
55
|
+
};
|
|
56
|
+
this.sessions.set(id, session); // 写入内存缓存
|
|
57
|
+
return session;
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
* **物理优势**:当用户请求会话列表时,底座仅调用 `listSessions()` 输出轻量级的 `sessionIndices`,**磁盘 I/O 带宽消耗降低了 95% 以上**,页面加载瞬间完成。
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## 二、 物理防御:并发更新锁与异步排队机制
|
|
66
|
+
|
|
67
|
+
当大模型在并发执行多个工具(Parallel Tool Calls)时,这些工具的返回耗时不同。在 `Promise.all` 结束后,底座需要把它们的结果写入 Session。
|
|
68
|
+
或者,当高频交互下,用户瞬间发送了多条消息。如果不对并发写入进行约束,多个异步 `appendMessages` 方法会在几毫秒内同时执行:
|
|
69
|
+
1. **进程 A** 读出了旧的历史文件 $F$。
|
|
70
|
+
2. **进程 B** 也读出了相同的旧历史文件 $F$。
|
|
71
|
+
3. **进程 A** 向历史追加了消息 1,写入文件 $F$。
|
|
72
|
+
4. **进程 B** 向它读到的旧历史里追加了消息 2,**覆写了文件 $F$(导致消息 1 被无情覆盖,彻底物理丢失)**。
|
|
73
|
+
|
|
74
|
+
为了防范这一并发竞态隐患,Freya 创新地在内存中实现了一个**针对特定会话 ID 的异步单链更新锁**:
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
// 记录每个会话当前的异步写操作锁 Promise
|
|
78
|
+
private updateLocks = new Map<string, Promise<unknown>>();
|
|
79
|
+
|
|
80
|
+
private async enqueueWrite<T>(sessionId: string, fn: (session: Session) => Promise<T>): Promise<T> {
|
|
81
|
+
// 1. 获取当前会话已挂接的锁 Promise,若无,初始化为一个 Resolved Promise
|
|
82
|
+
const current = this.updateLocks.get(sessionId) ?? Promise.resolve();
|
|
83
|
+
|
|
84
|
+
// 2. 利用 .then() 挂载新的写操作,使其在旧锁 resolve 后才启动 (单链排队)
|
|
85
|
+
const next = current.then(async () => {
|
|
86
|
+
let session: Session | null = null;
|
|
87
|
+
try {
|
|
88
|
+
session = await this.lazyLoadSession(sessionId); // 确保读取到的是最新的物理状态
|
|
89
|
+
} catch {}
|
|
90
|
+
return fn(session as any); // 执行真正的数据更新 (如 push 消息)
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// 3. 将新锁覆盖更新回 Map,形成单链挂载
|
|
94
|
+
this.updateLocks.set(sessionId, next);
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
return await next; // 等待当前写任务排队执行完成并返回
|
|
98
|
+
} finally {
|
|
99
|
+
// 4. 清理已执行完的锁,释放内存
|
|
100
|
+
if (this.updateLocks.get(sessionId) === next) {
|
|
101
|
+
this.updateLocks.delete(sessionId);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### 物理设计优势:
|
|
108
|
+
这是一种在 JS 单线程事件循环下极其老练的**协程互斥锁(Mutex)**设计。它不需要引入外部复杂的 Redis 锁,仅仅通过内存中的 Promise 链,就物理杜绝了多会话高并发下任何 I/O 竞态条件,保证了会话写入的绝对原子性。
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## 三、 非阻塞式后置异步压缩调度
|
|
114
|
+
|
|
115
|
+
在 `appendMessages` 的后半段,一旦完成助理消息 `hasAssistant` 的追加,底座会触发**非阻塞式的后置压缩流程**。
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
if (hasAssistant) {
|
|
119
|
+
// 💡 异步调度:启动压缩计算,但绝不 await!
|
|
120
|
+
// 主推理进程在 append 完消息后立刻返回给前端用户,将慢速的压缩计算丢到后台非阻塞执行
|
|
121
|
+
this.compactor.compressPostChat(session, session.modelId).then(async (result) => {
|
|
122
|
+
if (result) {
|
|
123
|
+
// 压缩完毕,重新排队写入,安全合并入历史
|
|
124
|
+
await this.enqueueWrite(sessionId, async (latestSession) => {
|
|
125
|
+
...
|
|
126
|
+
latestSession.summary = taggedSummary;
|
|
127
|
+
latestSession.history = [summaryUserMsg, ...keepMessages];
|
|
128
|
+
await this.persistence.saveSessionData(latestSession);
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}).catch((err) => {
|
|
132
|
+
this.logger?.error(`[SessionManager] 后置异步会话压缩发生异常:`, err);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
这种后置异步调度极大优化了交互响应速度。用户在收到回答后,底座在后台默默进行“记忆整理与淘汰”,整个过程完全不对用户的首字响应延时造成任何干扰。
|
|
138
|
+
|
|
139
|
+
本节我们白盒看清了 `SessionManager` 底层的延迟加载、并发写排队锁与后置异步压缩。在下一小节中,我们将亲自在本地进行调试,去排查多会话高并发下可能引发的状态脏读大坑。
|