@eoasmxd/freya 0.3.0 → 0.4.1

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.
Files changed (114) hide show
  1. package/README.md +9 -3
  2. package/core/dist/command/commands/skill-commands.js +5 -4
  3. package/core/dist/config/config-manager.d.ts +5 -1
  4. package/core/dist/config/config-manager.js +13 -1
  5. package/core/dist/kernel.js +2 -2
  6. package/core/dist/skill/skill-registry.d.ts +12 -2
  7. package/core/dist/skill/skill-registry.js +89 -8
  8. package/core/dist/tools/meta/index.d.ts +3 -1
  9. package/core/dist/tools/meta/index.js +9 -1
  10. package/core/dist/web/config-api.js +18 -0
  11. package/core/package.json +2 -2
  12. package/doc/_index.md +20 -7
  13. package/doc/getting-started.md +1 -1
  14. package/doc/installation-guide.md +2 -2
  15. package/doc/{architecture-design.md → specifications/architecture-design.md} +7 -4
  16. package/doc/{config-spec.md → specifications/config-spec.md} +1 -1
  17. package/doc/{llm-interface-params.md → specifications/llm-interface-params.md} +8 -8
  18. package/doc/{prompt-system.md → specifications/prompt-system.md} +1 -1
  19. package/doc/tutorials/_index.md +96 -0
  20. package/doc/tutorials/part0_basic/0.1_probability_prediction.md +155 -0
  21. package/doc/tutorials/part0_basic/0.2_attention_and_context.md +145 -0
  22. package/doc/tutorials/part0_basic/0.3_generation_parameters.md +132 -0
  23. package/doc/tutorials/part0_basic/0.4_debugging_token.md +99 -0
  24. package/doc/tutorials/part0_basic/1.1_stateless_and_history.md +143 -0
  25. package/doc/tutorials/part0_basic/1.2_chat_data_structure.md +179 -0
  26. package/doc/tutorials/part0_basic/1.3_system_user_assistant.md +147 -0
  27. package/doc/tutorials/part0_basic/1.4_freya_model_proxy.md +203 -0
  28. package/doc/tutorials/part0_basic/_index.md +28 -0
  29. package/doc/tutorials/part1_react/2.1_agency_vs_chatbot.md +127 -0
  30. package/doc/tutorials/part1_react/2.2_react_mind_model.md +144 -0
  31. package/doc/tutorials/part1_react/2.3_freya_agent_executor.md +233 -0
  32. package/doc/tutorials/part1_react/2.4_debugging_loop_deadlock.md +160 -0
  33. package/doc/tutorials/part1_react/3.1_hardcoded_prompt_pain.md +104 -0
  34. package/doc/tutorials/part1_react/3.2_decoupled_architecture.md +138 -0
  35. package/doc/tutorials/part1_react/3.3_freya_dual_read_probe.md +152 -0
  36. package/doc/tutorials/part1_react/3.4_debugging_composition_placeholder.md +97 -0
  37. package/doc/tutorials/part1_react/_index.md +28 -0
  38. package/doc/tutorials/part2_tools/4.1_json_schema_mapping.md +119 -0
  39. package/doc/tutorials/part2_tools/4.2_tool_call_raw_packet.md +105 -0
  40. package/doc/tutorials/part2_tools/4.3_freya_tool_execution.md +145 -0
  41. package/doc/tutorials/part2_tools/4.4_debugging_observation_fix.md +154 -0
  42. package/doc/tutorials/part2_tools/5.1_observation_injection.md +131 -0
  43. package/doc/tutorials/part2_tools/5.2_openai_vs_gemini_protocol.md +125 -0
  44. package/doc/tutorials/part2_tools/5.3_freya_llm_proxy_mapping.md +162 -0
  45. package/doc/tutorials/part2_tools/5.4_debugging_parallel_call_chaos.md +135 -0
  46. package/doc/tutorials/part2_tools/_index.md +28 -0
  47. package/doc/tutorials/part3_memory/6.1_session_state_lifecycle.md +143 -0
  48. package/doc/tutorials/part3_memory/6.2_physical_sandbox_separation.md +108 -0
  49. package/doc/tutorials/part3_memory/6.3_freya_session_storage.md +139 -0
  50. package/doc/tutorials/part3_memory/6.4_debugging_session_concurrency.md +161 -0
  51. package/doc/tutorials/part3_memory/7.1_context_overflow_loss.md +109 -0
  52. package/doc/tutorials/part3_memory/7.2_sliding_window_vs_summary.md +85 -0
  53. package/doc/tutorials/part3_memory/7.3_freya_compactor_impl.md +142 -0
  54. package/doc/tutorials/part3_memory/7.4_debugging_summarize_deadlock.md +160 -0
  55. package/doc/tutorials/part3_memory/_index.md +28 -0
  56. package/doc/tutorials/part4_streaming/8.1_sse_protocol_basics.md +119 -0
  57. package/doc/tutorials/part4_streaming/8.2_hiding_thoughts_in_stream.md +132 -0
  58. package/doc/tutorials/part4_streaming/8.3_freya_event_bus.md +104 -0
  59. package/doc/tutorials/part4_streaming/8.4_debugging_stream_decoder.md +158 -0
  60. package/doc/tutorials/part4_streaming/9.1_abort_signal_braking.md +162 -0
  61. package/doc/tutorials/part4_streaming/9.2_async_event_channels.md +142 -0
  62. package/doc/tutorials/part4_streaming/9.3_freya_abort_billing.md +122 -0
  63. package/doc/tutorials/part4_streaming/9.4_debugging_abort_lock_deadlock.md +187 -0
  64. package/doc/tutorials/part4_streaming/_index.md +28 -0
  65. package/doc/tutorials/part5_plugins/10.1_microkernel_decoupling.md +142 -0
  66. package/doc/tutorials/part5_plugins/10.2_plugin_metadata_security.md +125 -0
  67. package/doc/tutorials/part5_plugins/10.3_channel_plugin_development.md +149 -0
  68. package/doc/tutorials/part5_plugins/10.4_debugging_channel_reconnection.md +160 -0
  69. package/doc/tutorials/part5_plugins/_index.md +21 -0
  70. package/doc/tutorials/part6_advanced/11.1_react_model_flaws.md +111 -0
  71. package/doc/tutorials/part6_advanced/11.2_reflexion_mind_model.md +103 -0
  72. package/doc/tutorials/part6_advanced/11.3_reflexion_hands_on.md +182 -0
  73. package/doc/tutorials/part6_advanced/11.4_debugging_reflexion_convergence.md +108 -0
  74. package/doc/tutorials/part6_advanced/12.1_single_agent_limits.md +100 -0
  75. package/doc/tutorials/part6_advanced/12.2_multi_agent_patterns.md +121 -0
  76. package/doc/tutorials/part6_advanced/12.3_freya_multi_agent_routing.md +143 -0
  77. package/doc/tutorials/part6_advanced/12.4_multi_agent_hands_on.md +176 -0
  78. package/doc/tutorials/part6_advanced/_index.md +28 -0
  79. package/doc/tutorials/preface.md +30 -0
  80. package/package.json +3 -2
  81. package/plugins/plugin-gemini/package.json +1 -1
  82. package/plugins/plugin-openai/package.json +1 -1
  83. package/plugins/plugin-telegram-channel/package.json +1 -1
  84. package/plugins/plugin-tool-fs/package.json +1 -1
  85. package/plugins/plugin-tool-memory/package.json +1 -1
  86. package/plugins/plugin-tool-mysql/config/prompts/plugin.prompt.mysql.md +9 -0
  87. package/plugins/plugin-tool-mysql/config/prompts/plugin.prompt.mysql.select.audit.md +26 -0
  88. package/plugins/plugin-tool-mysql/dist/audit.d.ts +13 -0
  89. package/plugins/plugin-tool-mysql/dist/audit.js +87 -0
  90. package/plugins/plugin-tool-mysql/dist/index.d.ts +14 -0
  91. package/plugins/plugin-tool-mysql/dist/index.js +34 -0
  92. package/plugins/plugin-tool-mysql/dist/pool-manager.d.ts +32 -0
  93. package/plugins/plugin-tool-mysql/dist/pool-manager.js +113 -0
  94. package/plugins/plugin-tool-mysql/dist/tools.d.ts +11 -0
  95. package/plugins/plugin-tool-mysql/dist/tools.js +88 -0
  96. package/plugins/plugin-tool-mysql/package.json +33 -0
  97. package/plugins/plugin-tool-mysql/schema.json +83 -0
  98. package/plugins/plugin-tool-web/package.json +1 -1
  99. package/plugins/plugin-wecom-channel/package.json +1 -1
  100. package/plugins/plugin-weixin-channel/package.json +1 -1
  101. package/src/packages/core/src/command/commands/skill-commands.ts +6 -5
  102. package/src/packages/core/src/config/config-manager.ts +15 -1
  103. package/src/packages/core/src/kernel.ts +3 -2
  104. package/src/packages/core/src/skill/skill-registry.ts +100 -8
  105. package/src/packages/core/src/tools/meta/index.ts +9 -1
  106. package/src/packages/core/src/web/config-api.ts +20 -0
  107. package/src/packages/ui/src/features/config/ConfigModal.tsx +13 -1
  108. package/src/packages/ui/src/features/config/panels/SkillConfigPanel.tsx +137 -0
  109. package/src/plugins/plugin-tool-mysql/src/audit.ts +103 -0
  110. package/src/plugins/plugin-tool-mysql/src/index.ts +44 -0
  111. package/src/plugins/plugin-tool-mysql/src/pool-manager.ts +132 -0
  112. package/src/plugins/plugin-tool-mysql/src/tools.ts +100 -0
  113. package/ui/assets/{index-Be0cAgdB.js → index-BqPQMflk.js} +14 -14
  114. package/ui/index.html +1 -1
@@ -0,0 +1,28 @@
1
+ ---
2
+ title: "第三部分:记忆与上下文"
3
+ weight: 40
4
+ bookCollapseSection: true
5
+ ---
6
+
7
+ # 第三部分:大脑的心流与记忆机制 —— 会话与上下文管理
8
+
9
+ 本部分将深入解密智能体的大脑记忆系统。我们将探究会话(Session)在 Agent 系统中的生命周期、数据与源码物理隔离架构设计,以及滑动窗口与基于 LLM 的主动摘要压缩算法。
10
+
11
+ ---
12
+
13
+ ## 🧭 章节导学与阅读清单
14
+
15
+ ### 💾 第 6 章:智能体的短期记忆、多轮会话与持久化安全隔离
16
+ 探索会话在 Agent 架构中的生命周期与心流跟踪,理解配置、持久化数据与项目源码在物理上的隔离防线。
17
+ * 👉 **[6.1 会话管理生命周期与数据结构](6.1_session_state_lifecycle.md)**
18
+ * 👉 **[6.2 【沙箱设计】数据与源码物理隔离](6.2_physical_sandbox_separation.md)**
19
+ * 👉 **[6.3 【白盒剖析】物理持久化存储机制](6.3_freya_session_storage.md)**
20
+ * 👉 **[6.4 调试与避坑指南:高并发会话串线与写入锁排查](6.4_debugging_session_concurrency.md)**
21
+
22
+ ### ⏳ 第 7 章:上下文窗口溢出危机:压缩、滚动与摘要算法
23
+ 分析大模型发生严重遗忘与 Token 耗费崩塌的本质原因,并拆解滑动窗口截断、LLM 主动摘要生成以及闲置工具淘汰等压缩算法。
24
+ * 👉 **[7.1 上下文溢出的毁灭性后果与窗口危机](7.1_context_overflow_loss.md)**
25
+ * 👉 **[7.2 上下文压缩机制:滑动窗口与递归摘要算法](7.2_sliding_window_vs_summary.md)**
26
+ * 👉 **[7.3 【白盒剖析】Compactor 与淘汰算法](7.3_freya_compactor_impl.md)**
27
+ * 👉 **[7.4 调试与避坑指南:递归摘要“套娃死锁”防御](7.4_debugging_summarize_deadlock.md)**
28
+
@@ -0,0 +1,119 @@
1
+ ---
2
+ title: "8.1 SSE 协议本质与首字延迟革命"
3
+ weight: 10
4
+ description: "探秘流式输出在智能体交互中的核心地位,解剖 SSE 协议底层原理,解决 Nginx 反向代理下的“憋尿效应”缓冲堵塞陷阱。"
5
+ ---
6
+
7
+ # 8.1 SSE 协议本质与首字延迟革命
8
+
9
+ 大语言模型(LLM)基于自回归机制,其预测和生成 Token 是一个逐字吐出的物理过程。
10
+
11
+ 如果我们的智能体底座在处理大模型响应时,采取的是传统的“同步阻断式(Blocking)”通信 —— 必须等待大模型源源不断地生成完 1000 字的完整回答后,才把整个 JSON 一次性返回给客户端。
12
+
13
+ * **后果**:前端界面会陷入长达 20 至 30 秒的死寂转圈状态。对于用户而言,超过 3 秒没有响应,页面流失率就会呈指数级拉升。
14
+
15
+ 为了彻底消除用户的“响应焦虑”,现代智能体底座的通信架构必须完全转为流式驱动。而其中最核心的物理动脉,就是 **SSE (Server-Sent Events,服务器发送事件) 协议**。
16
+
17
+ 本节我们将深入解剖 SSE 协议的底层原理解析,对比其与 WebSockets 的权衡,并解决在云端部署时最经典的“Nginx 憋尿式缓冲堵塞”陷阱。
18
+
19
+ ---
20
+
21
+ ## 一、 首字延迟 (TTFT) 优先的交互革命
22
+
23
+ 流式输出在智能体开发中,不是一个“可选的锦上添花配置”,而是**关乎系统生死的基石**。
24
+
25
+ 在评估智能体性能时,行业最重视的指标是 **TTFT (Time-to-First-Token,首字渲染延迟)**。
26
+
27
+ ```
28
+ [同步阻断式] ──> 【发送请求】 ──────等待 30 秒并燃烧上下文──────> 【一次性吐出 1000 字】 (体验极差)
29
+
30
+ [流式 SSE 式] ──> 【发送请求】 ──等待 0.4 秒──> 【吐出第1字】 ──逐字流淌 30 秒──> 【流式完结】 (体验极佳)
31
+ ```
32
+
33
+ 只要大模型吐出第一个 Token,底座网络层在 0.5 秒内将其推送到用户的屏幕上,即使模型后面还需要生成 1 分钟,用户也能感知到智能体“正在以人类阅读的速度思考和打字”。这在人机交互心理学上建立了强大的连贯性。
34
+
35
+ ---
36
+
37
+ ## 二、 什么是 SSE 协议?底层的单向长链接
38
+
39
+ 为了实现高频的字符推送,我们不需要拉起笨重的全双工通道。**SSE (Server-Sent Events)** 是一种标准的、基于 HTTP 协议的**轻量级单向长链接**推送技术。
40
+
41
+ ### 1. 对比 WebSockets 的工程权衡
42
+ * **WebSockets (双全工)**:
43
+ * *特点*:支持客户端与服务端双向对等高频发包。
44
+ * *痛点*:协议复杂,需要握手升级(HTTP 101 Switching Protocols),容易被公司防火墙或代理拦截;且对负载均衡和连接保活要求苛刻。
45
+ * **SSE (单向长链接)**:
46
+ * *特点*:**天然兼容普通 HTTP GET/POST 协议**。客户端只发一次请求,服务端保持连接不断,以流的形式单向不断给客户端喂数据。
47
+ * *优势*:极轻量,天然支持断线重连(`Last-Event-ID` 机制),天生免疫大多数企业防火墙的拦截,最适配大模型“单向高频吐字”的物理场景。
48
+
49
+ ### 2. SSE 协议在 HTTP 报文上的物理特征
50
+ 当底座向前端发起 SSE 流式推送时,它会在 HTTP 响应头中做出如下物理声明:
51
+
52
+ ```http
53
+ HTTP/1.1 200 OK
54
+ Content-Type: text/event-stream; charset=utf-8 <-- 💡 指示当前连接为流式事件流
55
+ Cache-Control: no-cache <-- 💡 禁止任何中间缓存缓存
56
+ Connection: keep-alive <-- 💡 强行保持 TCP 物理长链接不断
57
+ Transfer-Encoding: chunked <-- 💡 分块分段传输数据字节
58
+ ```
59
+
60
+ ---
61
+
62
+ ## 三、 SSE 抓包数据流的物理标本
63
+
64
+ 在底座的长链接管道中,数据是以特定格式的文本块流淌的。每条消息由一个或多个 `key: value` 行组成,并**必须以两个连续换行符 `\n\n` 作为一条消息的物理结束标志**。
65
+
66
+ 以下是一个标准的大模型吐字 SSE 原始数据包标本:
67
+
68
+ ```
69
+ event: message
70
+ data: {"choices":[{"delta":{"content":"你"}}]}
71
+
72
+ event: message
73
+ data: {"choices":[{"delta":{"content":"好"}}]}
74
+
75
+ event: message
76
+ data: {"choices":[{"delta":{"content":","}}]}
77
+
78
+ event: message
79
+ data: {"choices":[{"delta":{"content":"我是"}}]}
80
+
81
+ event: message
82
+ data: [DONE]
83
+
84
+
85
+ ```
86
+
87
+ * **物理分割标志**:客户端接收解析器在扫描字节流时,一旦读到 `\n\n`,就会把前面的 buffer 切下来作为一个完整的 `data` 段,调用 `JSON.parse` 提取 `choices[0].delta.content`,然后追加到屏幕上。
88
+ * **物理完结标志**:当大模型推理结束,底座会发送一个约定的终止符 `data: [DONE]\n\n`,告知客户端可以安全关闭连接。
89
+
90
+ ---
91
+
92
+ ## 四、 【避坑指南】解决 Nginx 反向代理下的“憋尿效应” (Buffering)
93
+
94
+ 很多智能体开发者在本地(`localhost`)调试时,流式吐字非常丝滑,可一旦打包部署到云端服务器,通过域名访问时,流式直接瘫痪 —— 页面卡住 20 秒,然后**突然“憋尿式”地把所有文字一次性喷涌吐出来**。
95
+
96
+ ### 1. 物理成因:Nginx Proxy Buffering
97
+ 大部分云端生产环境都挂载了 Nginx 反向代理。Nginx 默认开启了 **代理缓冲区(Proxy Buffering)** 机制。
98
+
99
+ 当 Nginx 接收到后端底座发来的 SSE 数据块时,它觉得“块太小了(只有几十字节)”,出于网络传输效率优化,它会**强行拦截这些小块,并在内存里攒够 4kb 数据后才一次性统一打包发送给前端用户**。
100
+
101
+ 这就把“细水长流”的 SSE 直接堵塞成了“大坝泄洪”,首字时间(TTFT)彻底破产。
102
+
103
+ ### 2. 避坑策略:注入 X-Accel-Buffering 响应头
104
+ 要彻底消灭这一代理缓冲堵塞,底座在向前端输出流式 HTTP 响应时,**必须在 HTTP Header 中强行注入 `X-Accel-Buffering: no` 头部**:
105
+
106
+ ```typescript
107
+ // 💡 Node.js Web 服务器中强行破除 Nginx 缓冲的响应头注入
108
+ res.writeHead(200, {
109
+ 'Content-Type': 'text/event-stream',
110
+ 'Cache-Control': 'no-cache',
111
+ 'Connection': 'keep-alive',
112
+ 'X-Accel-Buffering': 'no' // 💡 致命防线:强行通知上游 Nginx 物理关闭缓冲,实时向下透传字节!
113
+ });
114
+ ```
115
+
116
+ 当 Nginx 读到 `X-Accel-Buffering: no` 这一标头时,会瞬间死心,放弃任何缓存攒批幻想,有多少字节就实时向浏览器透传多少字节,保证了云端生产环境下的极致首字响应。
117
+
118
+ 本节我们解剖了流式输出的重要性和 SSE 协议的底层原理解析。在下一小节中,我们将探讨在 ReAct 多轮循环中,如何优雅地拦截和隐藏大模型内部的思考(Thought)与工具调用流,只向用户暴露最纯净的最终回答。
119
+
@@ -0,0 +1,132 @@
1
+ ---
2
+ title: "8.2 隐藏中间推理的工程艺术"
3
+ weight: 20
4
+ description: "探讨如何在流式通道中过滤并隐藏中间 Tool Call 与 Observation 过程,设计流式状态机拦截器防御思维标签泄露。"
5
+ ---
6
+
7
+ # 8.2 隐藏中间推理的工程艺术
8
+
9
+ 在 8.1 节中,我们了解了 SSE 协议逐字推流的底层本质。一旦我们将大模型置于 ReAct 决策自循环(while-loop)中,智能体的大脑运作就会变得非常喧嚣:
10
+ 1. 大模型生成 `Action`:`“我决定调用 read_file,参数是 path='notes.txt'”`。
11
+ 2. 底座执行并返回 `Observation`:`“[文件内容]: 1. 整理季度架构总结...”`。
12
+ 3. 大模型纠错、再次思考(Thought)并输出最终文本。
13
+
14
+ 如果底座没有对推流管道进行精细控制,将大模型吐出的所有字节流无差别地推向前端:
15
+
16
+ * **毁灭性的用户交互**:用户的屏幕上会突然闪过一连串的技术代码、JSON 结构和沙箱路径。这会让非技术用户感到极其迷惑和恐慌。
17
+ * **重大的商业与安全风险**:智能体在后台调用的内部工具名称、参数结构与本地路径,会被前端抓包暴露。
18
+
19
+ 本节我们将探讨如何在底座中建立**双轨管道过滤(Dual-Pipe Filtering)**,隐藏大脑内部调用的细节,只将优雅的执行进度与最终文本推送给用户。
20
+
21
+ ---
22
+
23
+ ## 一、 双轨管道过滤 (Dual-Pipe Filtering) 架构
24
+
25
+ 为了在输出字符时“既有深度,又无噪音”,Freya 在事件总线层与大模型流式层建立起两条**绝对隔离的交互管道**:
26
+
27
+ ```
28
+ ┌──> 系统事件通道 (EventBus) ──> tool:status 状态广播 (UI 进度条)
29
+
30
+ [大模型 ReAct 决策流] ──> 【底座双轨分流机制】
31
+
32
+ └──> 用户流式通道 (SSE Stream) ──> 最终回答逐字推流 (User Only)
33
+ ```
34
+
35
+ ### 1. 系统事件通道 (EventBus 状态广播)
36
+ 底座在内部执行 ReAct 决策循环时,当命中 `toolCalls` 并触发工具执行,通过 `eventBus.emit('tool:status', { sessionId, toolName, status: 'running' })` 向全系统广播生命周期状态。
37
+ 前台 Web UI 或外部通道监听这些结构化 Event 流,将其转化为精美的人性化进度条(如显示一行微光动画:“*正在读取文件 notes.txt...*”),而不是直接显示 Raw JSON。
38
+
39
+ ### 2. 用户流式通道 (协议级条件分流)
40
+ 在 `plugins/plugin-openai/src/index.ts` 中,Freya 设计了极为精准的**流式条件分流开关**:
41
+
42
+ ```typescript
43
+ // 摘自 plugins/plugin-openai/src/index.ts
44
+ const isStream = !!options?.onChunk && (!tools || tools.length === 0);
45
+ if (isStream) {
46
+ requestBody.stream = true;
47
+ requestBody.stream_options = { include_usage: true };
48
+ }
49
+ ```
50
+
51
+ * **决策与工具调用轮次**:由于携带了 `tools` 列表,系统不开启直接面向用户的流式传输,而是由内核完整接收结构化 Tool Call 并执行工具,通过 EventBus 发送进度事件。
52
+ * **最终收敛回答轮次**:当大模型完成所有工具调用,进入无工具模式(`!tools || tools.length === 0`)输出最终答案时,底座才激活 `requestBody.stream = true`,逐字推流给前端用户,实现极致的 TTFT 体验。
53
+
54
+ ---
55
+
56
+ ## 二、 隐藏推理流 (Hidden Thinking) 的工程实现
57
+
58
+ 现代前沿模型(如 DeepSeek-R1、OpenAI o1/o3)引入了专门的“深度思考流(Reasoning Content)”,在 API 返回中通过 `reasoning_content` 与正常的 `content` 分离返回。
59
+
60
+ 然而,对于大多数不支持该特性的普通大模型,它们通常会将思考过程混在 `content` 中,并用特殊的 Markdown 标记包裹,例如:
61
+ `<thought> 我需要先查一下他的订单,再查他的工资... </thought> 您好,我已经为您查到了...`
62
+
63
+ 底座必须能够在流式输出中,实时将 `<thought>...</thought>` 中的文本剥离并拦截,只把后半段推给用户。
64
+
65
+ ---
66
+
67
+ ## 三、 【避坑指南】防止被切碎的 XML 标签引发前端页面失控
68
+
69
+ 在大模型推流时,字符是极度碎裂地返回的(比如一两个字符一两个字符地吐)。如果你试图用简单的 `string.includes('</thought>')` 来判断是否结束,会踩进一个**“标签跨 Chunk 被切碎”**的致命工程坑中。
70
+
71
+ ### 1. 标签切碎泄露事故
72
+ 假设大模型输出流在截断边界被切分为了两个连续 Chunk:
73
+ * **Chunk A**:`“...正在查询订单。</th”`
74
+ * **Chunk B**:`“ought> 您好,您的订单...”`
75
+
76
+ 如果你只在接收到每个 Chunk 时用 `includes('</thought>')` 检测:
77
+ * 由于 Chunk A 只有 `</th`,未命中结束标签,底座判定仍处于思考区,将其拦截。
78
+ * 由于 Chunk B 只有 `ought>`,也未命中 `</thought>`,底座判定仍处于思考区,将其拦截。
79
+ * **后果**:因为整个结束标示符被拦腰切断,底座**彻底迷失了状态**,导致大括号标签后的所有正常回答文本,全部被当成了思考过程进行拦截,前端用户永远收不到最终回答,智能体表现为“假死”。
80
+
81
+ ### 2. 防御策略:流式状态机拦截器 (Streaming State Machine)
82
+ 为了在流式中无损过滤思维流,底座必须引入一个**简单的状态机拦截器**。它不进行整串匹配,而是逐字进行字符探针状态转移:
83
+
84
+ ```typescript
85
+ export class StreamingThinkingParser {
86
+ private inThinking = false;
87
+ private tagBuffer = "";
88
+
89
+ // 状态机核心方法,接收逐字流,返回过滤后应该发送给用户的字符
90
+ feed(char: string): string {
91
+ // 1. 如果检测到可能是标签的开头
92
+ if (char === '<') {
93
+ this.tagBuffer = '<';
94
+ return ""; // 暂扣,不发送
95
+ }
96
+
97
+ if (this.tagBuffer.startsWith('<')) {
98
+ this.tagBuffer += char;
99
+
100
+ // 检测是否命中了思考开始标签 <thought>
101
+ if (this.tagBuffer === '<thought>') {
102
+ this.inThinking = true;
103
+ this.tagBuffer = "";
104
+ return "";
105
+ }
106
+
107
+ // 检测是否命中了思考结束标签 </thought>
108
+ if (this.tagBuffer === '</thought>') {
109
+ this.inThinking = false;
110
+ this.tagBuffer = "";
111
+ return "";
112
+ }
113
+
114
+ // 如果缓冲区长度已经超出了标签的最大可能长度,说明不是合法标签,回滚释放
115
+ if (this.tagBuffer.length > 10) {
116
+ const fallback = this.tagBuffer;
117
+ this.tagBuffer = "";
118
+ return this.inThinking ? "" : fallback;
119
+ }
120
+
121
+ return ""; // 仍处于标签识别缓冲期
122
+ }
123
+
124
+ // 2. 正常字符路由
125
+ return this.inThinking ? "" : char; // 若处于思考期,默默吞掉;否则安全输出
126
+ }
127
+ }
128
+ ```
129
+
130
+ 通过这一层字符级状态机,无论大模型吐出的字节流有多碎,`</thought>` 标签在物理层面上被切得有多烂,底座都能在毫秒级内将其精准捕获并将其静默蒸发,保障了用户交互端的极致干净。
131
+
132
+ 本节我们建立了双轨推流管道,并通过状态机隔离了大脑内部推理的喧嚣。在下一小节中,我们将实际切入 Freya 的消息通信总线,白盒剖析它是如何利用 EventEmitter 在整个底座广播这些状态事件的。
@@ -0,0 +1,104 @@
1
+ ---
2
+ title: "8.3 【白盒剖析】EventBus 事件广播与响应推送"
3
+ weight: 30
4
+ description: "白盒解剖 Freya 的 event-bus.ts 源码,探秘 Node.js EventEmitter 封装与事件重播缓冲区(Event Replay)物理实现。"
5
+ ---
6
+
7
+ # 8.3 【白盒剖析】EventBus 事件广播与响应推送
8
+
9
+ 在 8.2 节中,我们确立了双轨管道过滤(Dual-Pipe Filtering)的架构。底座拦截了用户可见的流式文本,将工具状态、计费更新、连接控制等一系列底层生命周期动作转化为结构化事件。
10
+
11
+ 在 Node.js 的微内核插件式架构中,内核与插件、插件与插件之间是严禁采用强耦合的方法调用的(这违反了单向边界硬约束)。它们唯一的物理媒介,就是**事件总线(EventBus)**。
12
+
13
+ 本节我们将实际白盒解剖 Freya 内核的事件总线实现(位于 `packages/core/src/event/event-bus.ts`),看懂它是如何基于 Node.js 原生 `EventEmitter` 进行二次包裹,并依靠**事件重播缓冲区(Replay Buffers)**优雅消灭“初始化竞争冲突”这一高难度硬核 Bug 的。
14
+
15
+ ---
16
+
17
+ ## 一、 FreyaEventBus:解耦核心与外部插件的物理媒介
18
+
19
+ 在 `packages/core/src/event/event-bus.ts` 中,`FreyaEventBus` 实现了 SDK 中的 `EventBus` 抽象接口。
20
+
21
+ 它内部包裹了 Node.js 原生的 `EventEmitter`:
22
+
23
+ ```typescript
24
+ import type { EventBus } from '@eoasmxd/freya-sdk';
25
+ import { EventEmitter } from 'node:events';
26
+
27
+ export class FreyaEventBus implements EventBus {
28
+ // 1. 物理包裹原生 EventEmitter 实例
29
+ private emitter = new EventEmitter();
30
+
31
+ // 2. 事件重播历史缓冲区
32
+ private replayBuffers = new Map<string, any[][]>();
33
+ }
34
+ ```
35
+
36
+ ### 为什么不直接用 `new EventEmitter()` 传给插件?
37
+ * **接口隔离原则**:如果直接给外部插件透传 Node 原生的 `EventEmitter`,插件就有权调用诸如 `removeAllListeners()` 这种极具破坏性的全局方法,瞬间瘫痪整个系统的事件广播线。
38
+ * **物理契约约束**:通过 `EventBus` 接口进行包裹隔离,底座仅暴露了安全的 `on`、`off` 和 `emit` 三个原子契约,锁死了外界篡改内核监听线的后门。
39
+
40
+ ---
41
+
42
+ ## 二、 解决初始化顺序冲突:事件重播缓冲区 (Event Replay)
43
+
44
+ 在微内核(Microkernel)与 Monorepo 插件加载体系中,我们会面临一个经典的**时序初始化大坑**:
45
+
46
+ ### 1. 物理时序竞争冲突 (Race Condition)
47
+ 系统在启动时,会扫描 `plugins/` 目录加载插件。
48
+ 假设**插件 A(LLM 计费模块)**最先被加载,它在初始化完成的瞬间,向总线 `emit('init:billing', { rate: 0.1 })` 广播了计费参数。
49
+ 然而,负责接收这些计费参数并显示在 UI 上的**插件 B(WebSockets 通道模块)**在 50 毫秒后才被加载就绪,随后调用了 `on('init:billing', ...)`。
50
+ * **灾难下场**:因为插件 B 的监听器注册晚了 50 毫秒,大牌子广播已经播放完毕。插件 B 彻底漏掉了这笔初始化参数,导致前台 UI 的计费面板显示为空白,发生状态不一致 Bug。
51
+
52
+ ### 2. 救赎:Freya 的重播缓冲区设计
53
+ `FreyaEventBus` 通过在 `emit` 和 `on` 之间插入一个 **Replay Buffer 历史缓存区**,物理消灭了这一时序冲突:
54
+
55
+ ```typescript
56
+ emit(event: string, ...args: any[]): void {
57
+ // 1. 正常广播当前活跃的监听器
58
+ this.emitter.emit(event, ...args);
59
+
60
+ // 2. 💡 物理过滤:只将含有 'register' 或 'init' 的系统根源事件存入缓冲区
61
+ const shouldBuffer = event.includes('register') || event.includes('init');
62
+ if (shouldBuffer) {
63
+ if (!this.replayBuffers.has(event)) {
64
+ this.replayBuffers.set(event, []);
65
+ }
66
+ // 将该事件的历史参数追加存盘
67
+ this.replayBuffers.get(event)!.push(args);
68
+ }
69
+ }
70
+ ```
71
+
72
+ 当任何插件在**未来较晚的某个时序点**调用 `on()` 注册监听时,重播防线会瞬间苏醒:
73
+
74
+ ```typescript
75
+ on(event: string, listener: (...args: any[]) => void): void {
76
+ // 1. 注册正常的常规事件监听
77
+ this.emitter.on(event, listener);
78
+
79
+ // 2. 💡 历史追溯:检测该事件名是否在 replay 缓冲区中有未读历史
80
+ const buffer = this.replayBuffers.get(event);
81
+ if (buffer) {
82
+ for (const args of buffer) {
83
+ // 3. 巧妙妙用 Node.js 的 setImmediate,将重播操作丢入事件循环的下一个 Tick 异步执行
84
+ // 绝对防止同步阻塞当前注册过程,消灭死锁
85
+ setImmediate(() => {
86
+ listener(...args); // 依次将遗漏的历史事件强行“塞”给新监听器
87
+ });
88
+ }
89
+ }
90
+ }
91
+ ```
92
+
93
+ ---
94
+
95
+ ## 三、 setImmediate 的工程妙用
96
+
97
+ 在重播机制中,为什么不能直接同步调用 `listener(...args)`,而要用 `setImmediate` 包裹?
98
+
99
+ 如果在 `on()` 中直接同步循环执行 `listener()`:
100
+ * 如果监听器内部又触发了其他连锁 `emit` 动作,会导致当前 `on()` 调用栈(Call Stack)陷入恐怖的**同步递归回环**。
101
+ * 这会发生 CPU 100% 爆满,或者因为超出 V8 最大调用栈深度直接报 `Maximum call stack size exceeded` 报错死机。
102
+ * **物理本质**:通过 `setImmediate`,底座告诉 Node.js:“先把当前的 `on` 注册完并释放控制流,在下一个 Tick(微步)里再去异步重播刚才的历史”。这在物理时序上实现了最安全的协程避让,保障了系统的极度平稳。
103
+
104
+ 本节我们白盒解刨了 `EventBus` 二次封装以及防止初始化时序冲突的事件重播缓冲区。在下一节中,我们将在本地进行调试,去排查在 TCP 物理传输分片中,由于“半个 UTF-8 字符”被切断而导致的流式传输中文乱码崩溃事故。
@@ -0,0 +1,158 @@
1
+ ---
2
+ title: "8.4 调试与避坑指南:反代缓存与流式 UTF-8 字符截断排错"
3
+ weight: 40
4
+ description: "实战调试 TCP 网络分片传输导致的流式中文乱码,利用 Node.js 内置 StringDecoder 建立高可靠字节流重组防线。"
5
+ ---
6
+
7
+ # 8.4 调试与避坑指南:反代缓存与流式 UTF-8 字符截断排错
8
+
9
+ 在前几节中,我们了解了 SSE 流式通信、隐藏推理的双轨推流设计,并剖析了 `EventBus` 的事件重播缓冲区。
10
+
11
+ 在智能体正式上线的实际运行中,有一个极富工程性的底层大坑随时会引爆交互灾难 —— **流式中文乱码问号黑块(Replacement Character )**:
12
+ * 在本地调试时,智能体中文吐字很正常。
13
+ * 部署到网络较差的移动 4G/5G 或高并发云端网关环境时,用户的手机屏幕上会时不时蹦出几个 `` 乱码黑问号(如:“你好,我是智能体”)。
14
+ * 一旦发生 `` 乱码,不仅前端排版崩溃,如果大模型在进行多轮 Tool Call 推导,**乱码的 JSON 字符串会导致 `JSON.parse` 报错,直接瘫痪整轮 ReAct 决策。**
15
+
16
+ 本节我们将实际解剖 TCP 网络分片与 UTF-8 字节截断的物理成因,并通过本地动手实验复现并彻底解决流式字节流解码乱码。
17
+
18
+ ---
19
+
20
+ ## 一、 TCP 分片与 UTF-8 “半个中文字符”乱码成因
21
+
22
+ 在 HTTP 块传输(Chunked Transfer-Encoding)中,底座在网络层接收到的数据并不是一段段优雅的完整字符串,而是大模型服务器通过 TCP/IP 协议切碎发来的 **二进制字节流片段 (Buffer Chunks)**。
23
+
24
+ ### 1. UTF-8 编码的物理事实
25
+ * 英文字符(ASCII):在 UTF-8 中只占用 1 个字节。
26
+ * 中文字符(汉字):在 UTF-8 中**通常占用 3 个字节**。
27
+ * 例如:汉字“我”的十六进制 UTF-8 编码是:`E6 88 91`(共 3 个字节)。
28
+
29
+ ### 2. TCP 盲目切刀截断
30
+ 在网络传输中,TCP 底层是完全不感知应用层语意字符的。网络缓冲区满或者网络拥堵时,TCP 的切刀会盲目剁下:
31
+
32
+ ```
33
+ 中文字符 “我” (E6 88 91)
34
+
35
+ ├───── 字节 E6 88 ─────> 网络分片 1 (Buffer 1)
36
+
37
+ └───── 字节 91 ─────> 网络分片 2 (Buffer 2)
38
+ ```
39
+
40
+ 如果底座的数据接收器在收到 Buffer 1 时,直接粗暴地调用了 `Buffer.toString('utf-8')` 或 `new TextDecoder().decode(Buffer 1)`:
41
+ * **物理悲剧**:由于 Buffer 1 的尾部只有 `E6 88`,属于残缺的不合法的 UTF-8 字符。
42
+ * 解码器尝试强行读取无果,**会彻底放弃并将这两个字节永久转化为 Unicode 乱码替代符 `` (Hex: EF BF BD)**。
43
+ * 即使下一毫秒 Buffer 2(包含最后一个字节 `91`)安全抵达并被解码,由于前面的两个字节已经被不可逆地变成了 ``,这 3 个字节再也无法在字符层面合成为一个正常的“我”字。用户只能在屏幕上看到一个刺眼的黑问号。
44
+
45
+ ---
46
+
47
+ ## 二、 本地调试:复现半字截断乱码
48
+
49
+ 我们在本地编写一段精准在字节层面执行剁刀的调试脚本:
50
+
51
+ ### 1. 乱码复现脚本 (decoder_chaos_test.js)
52
+ ```javascript
53
+ // 汉字 “我” 的 UTF-8 编码为 E6 88 91 (3个字节)
54
+ // 汉字 “是” 的 UTF-8 编码为 E6 98 AF (3个字节)
55
+ const originalText = "我是智能体";
56
+ const rawBuffer = Buffer.from(originalText, "utf-8");
57
+
58
+ console.log("原始字符串字节总数:", rawBuffer.length); // 15 字节
59
+
60
+ // 模拟网络分片:故意把 rawBuffer 剁成两半
61
+ // 第一片切在第 2 个字节末尾 (恰好把“我”字劈开,前 2 个字节分在 Chunk 1,后 1 个分在 Chunk 2)
62
+ const chunk1 = rawBuffer.subarray(0, 2);
63
+ const chunk2 = rawBuffer.subarray(2);
64
+
65
+ console.log("\n❌ Unsafe: 直接 toString() 还原数据包:");
66
+ const text1 = chunk1.toString("utf-8");
67
+ const text2 = chunk2.toString("utf-8");
68
+ // 拼接输出
69
+ console.log("拼接结果:", text1 + text2);
70
+ ```
71
+
72
+ ### 2. 物理乱码输出确证
73
+ 运行脚本,你会看到输出:
74
+ `拼接结果: 是智能体`
75
+ “我”字永久沦陷为乱码 ``,复现了网络分片带来的隐蔽灾难。
76
+
77
+ ---
78
+
79
+ ## 三、 防御实战:流式字节解码器 (StringDecoder 与 TextDecoder)
80
+
81
+ 为了解决这一物理截断大坑,底座网络解析层**绝对不能**对二进制分片直接调用 `toString()`。
82
+
83
+ Node.js 与 Web 标准环境分别提供了专门应对字节流重组的硬核武器:
84
+
85
+ ### 1. 解码器的物理重组机理
86
+ * 当调用 `decoder.write(chunk1)` 或 `decoder.decode(chunk1, { stream: true })` 时,它在底层进行 UTF-8 状态字节扫描。
87
+ * 如果检测到 Buffer 末尾残留了不合法的 UTF-8 字节(如 `E6 88`),它会**自动把这两个残存的字节扣留在内部的私有临时缓冲区(Internal Buffer)中**,只把前面完整解码的字符输出。
88
+ * 当下一毫秒调用 `decoder.write(chunk2)` 时,它将之前积压在私有缓冲区的字节与 `chunk2` 的开头自动缝合,解码出合法的汉字,完美消灭乱码。
89
+
90
+ ### 2. Node.js 本地测试脚本验证 (StringDecoder):
91
+ ```javascript
92
+ const { StringDecoder } = require("node:string_decoder");
93
+
94
+ // 实例化解码器,声明为 UTF-8
95
+ const decoder = new StringDecoder("utf-8");
96
+
97
+ console.log("\n✅ Safe: 使用 StringDecoder 进行重组解码:");
98
+
99
+ // 解码第一片 (此时它会自动把残缺的 2 个字节扣留在内存,输出为空字符)
100
+ const safeText1 = decoder.write(chunk1);
101
+ // 解码第二片 (缝合之前被扣留的 2 字节,成功释出“我”字并续上后文)
102
+ const safeText2 = decoder.write(chunk2);
103
+
104
+ console.log("拼接结果:", safeText1 + safeText2);
105
+ ```
106
+
107
+ 运行重构后的脚本,输出变为了完美无瑕的:
108
+ `拼接结果: 我是智能体`
109
+
110
+ ---
111
+
112
+ ## 四、 避坑经验:Freya 真实源码中的行缓冲器设计
113
+
114
+ 在完成流式 UTF-8 字符重组后,底座还不能直接对字符串调用 `JSON.parse`。因为在网络推流中,多个 SSE 事件在网络包里可能会粘在一起发过来(粘包现象),或者一个完整的 `data: {...}\n\n` 被腰斩成多截。
115
+
116
+ 在 `plugins/plugin-openai/src/index.ts` 中,Freya 结合 Web 标准 **`TextDecoder` 流式模式** 与 **行缓冲器(Line-buffered Parser)** 建立了严密的防线:
117
+
118
+ ```typescript
119
+ // 摘自 plugins/plugin-openai/src/index.ts
120
+ const reader = response.body.getReader();
121
+ const decoder = new TextDecoder();
122
+ let buffer = '';
123
+
124
+ while (true) {
125
+ const { done, value } = await reader.read();
126
+ if (done) break;
127
+
128
+ // 1. 💡 物理流式重组:开启 { stream: true },自动保留末尾残缺字节
129
+ buffer += decoder.decode(value, { stream: true });
130
+
131
+ // 2. 💡 行缓冲切分:以 \n 拆分消息行,并将未完结的半行重新保留到 buffer
132
+ const lines = buffer.split('\n');
133
+ buffer = lines.pop() || '';
134
+
135
+ // 3. 逐行解析合法的 SSE 事件
136
+ for (const line of lines) {
137
+ const trimmed = line.trim();
138
+ if (!trimmed || trimmed === 'data: [DONE]') continue;
139
+ if (trimmed.startsWith('data: ')) {
140
+ try {
141
+ const parsed = JSON.parse(trimmed.slice(6));
142
+ const text = parsed.choices?.[0]?.delta?.content;
143
+ if (text) {
144
+ finalContent += text;
145
+ options?.onChunk?.(text);
146
+ }
147
+ } catch {}
148
+ }
149
+ }
150
+ }
151
+ ```
152
+
153
+ 这套 `TextDecoder({ stream: true })` + 行缓冲区双重防御规范,保证了智能体流式通道无论在多么糟糕的网络丢包和分片环境下,字节都绝不会发生一位偏位或乱码,奠定了底座坚如磐石的流式高可靠性。
154
+
155
+ 本节我们通过流式解码器与行缓冲区攻克了 TCP 分片传输中文乱码与粘包问题。
156
+
157
+ 在下一章中,我们将进入交互链路的安全控制大门,去设计大模型失控时的“链路级刹车闸”以及多通道适配器。
158
+