@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,152 @@
1
+ ---
2
+ title: "3.3 【白盒剖析】双通道动态提示词合并"
3
+ weight: 30
4
+ description: "白盒解剖 Freya 的 prompt-registry.ts 源码,探秘双通道探针机制(Dual-Read)与多源 System Prompt 动态编织逻辑。"
5
+ ---
6
+
7
+ # 3.3 【白盒剖析】双通道动态提示词合并
8
+
9
+ 在 3.2 节中,我们建立了将提示词模板以下沉的 Markdown 文件形式存储在磁盘上的物理规范。然而,在智能体的实际部署中,我们面临着多环境覆盖的诉求:我们希望系统内置一套“开箱即用”的默认提示词;同时,又希望用户能在不修改只读源码包的前提下,在运行时通过在特定目录放置自定义文件来覆盖这些提示词。
10
+
11
+ 本节我们将实际解剖 Freya 底座中负责这一生命周期管理的核心模块 —— **提示词注册表**(位于 `packages/core/src/prompt/prompt-registry.ts`)。
12
+
13
+ 我们将一起看懂它是如何通过**双通道探针机制(Dual-Read)**进行文件的加载避让,以及如何将时间时区、工具附加指示和插件技能卡动态合成为一个单体系统提示词的。
14
+
15
+ ---
16
+
17
+ ## 一、 双通道探针机制 (Dual-Read) 的物理实现
18
+
19
+ 为了实现“内置只读,外置覆盖”的优雅架构,`FreyaPromptRegistry` 中定义了 `register()` 方法。
20
+
21
+ 在注册每一个提示词时,它都会启动两个物理探针去触碰磁盘文件:
22
+
23
+ ```typescript
24
+ export interface FreyaPrompt {
25
+ key: string;
26
+ content: string;
27
+ defaultPath: string; // 包内默认路径 (只读)
28
+ runPath?: string; // 用户运行时覆盖路径 (可写)
29
+ }
30
+
31
+ export class FreyaPromptRegistry {
32
+ private prompts = new Map<string, FreyaPrompt>();
33
+
34
+ /**
35
+ * 注册提示词元数据声明并执行异步双读载入
36
+ */
37
+ async register(prompt: Omit<FreyaPrompt, 'content'>): Promise<void> {
38
+ const runFilePath = this.getRunFilePath(prompt);
39
+ let content = '';
40
+
41
+ try {
42
+ try {
43
+ // 通道 1:探针尝试读取用户运行目录下的覆盖配置 (config/ 目录)
44
+ content = await fs.readFile(runFilePath, 'utf-8');
45
+ } catch {
46
+ // 通道 2:若通道 1 抛出文件不存在异常,降级加载只读的程序包默认配置
47
+ content = await fs.readFile(prompt.defaultPath, 'utf-8');
48
+ }
49
+ } catch {}
50
+
51
+ this.prompts.set(prompt.key, {
52
+ key: prompt.key,
53
+ content: content.trim(),
54
+ defaultPath: prompt.defaultPath,
55
+ runPath: prompt.runPath
56
+ });
57
+ }
58
+ }
59
+ ```
60
+
61
+ ### 物理设计优势:
62
+ 这种双读探针(Dual-Read)的设计在生产部署中极为强大:
63
+ 1. **物理防呆与降级**:即使管理员在运行时把外置的配置文件误删了,底座也不会挂起报错,而是会自动回退加载只读包内的默认模板。
64
+ 2. **安全隔离**:核心程序包(`APP_ROOT`)可以被设置为只读挂载(在 Docker 容器等环境中),而所有用户配置(`PROJECT_ROOT`)写在隔离的宿主持久化目录中。
65
+
66
+ ---
67
+
68
+ ## 二、 基础系统提示词的内存编织
69
+
70
+ 当所有的系统核心提示词(`identity` 身份, `soul` 灵魂, `tools` 工具, `agents` 拓扑, `user` 用户画像, `memory` 长期记忆)通过双通道加载进 Map 后,`getSystemPrompt()` 会负责它们的第一层拼接。
71
+
72
+ 在此过程中,它展示了底座如何向大模型动态注入**“真实时间与时区”**:
73
+
74
+ ```typescript
75
+ getSystemPrompt(): string {
76
+ const identity = this.get('core.prompt.identity');
77
+ const soul = this.get('core.prompt.soul');
78
+ const user = this.get('core.prompt.user');
79
+ const memory = this.get('core.prompt.memory');
80
+ const tools = this.get('core.prompt.tools');
81
+ const agents = this.get('core.prompt.agents');
82
+
83
+ // 1. 动态抓取当前操作系统的本地时区
84
+ const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone || 'Asia/Shanghai';
85
+ const now = new Date();
86
+ const nowStr = now.toLocaleString('zh-CN', { timeZone });
87
+ const weekday = ['日', '一', '二', '三', '四', '五', '六'][now.getDay()];
88
+
89
+ // 2. 计算时区偏差偏移量 (如 +8)
90
+ const offsetMinutes = -now.getTimezoneOffset();
91
+ const sign = offsetMinutes >= 0 ? '+' : '-';
92
+ const absHours = Math.floor(Math.abs(offsetMinutes) / 60);
93
+ const absMins = Math.abs(offsetMinutes) % 60;
94
+ const utcOffset = `UTC${sign}${absHours}${absMins > 0 ? `:${absMins.toString().padStart(2, '0')}` : ''}`;
95
+
96
+ const timeStr = `${nowStr} (星期${weekday}, 时区: ${timeZone}, ${utcOffset})`;
97
+
98
+ // 3. 多维度标准 Markdown 拼接
99
+ return `# IDENTITY (本体)\n${identity}\n\n` +
100
+ `# SOUL (灵魂)\n${soul}\n\n` +
101
+ `# USER INFO (用户画像)\n${user}\n\n` +
102
+ `# MEMORY (长期记忆)\n${memory}\n\n` +
103
+ `# TOOLS SPEC (工具指南)\n${tools}\n\n` +
104
+ `# AGENT TOPOLOGY (拓扑模式)\n${agents}\n\n` +
105
+ `# CURRENT TIME (当前时间)\n${timeStr}`;
106
+ }
107
+ ```
108
+
109
+ 自回归大模型无法感知物理世界的时间流逝。如果不在 System Prompt 头部注入当前时间,当用户问“我昨天发的那封警告信,今天到期了吗?”,大模型会因为缺乏时间基准给出错误的接龙概率。Freya 通过同步抓取操作系统时区偏移量,在物理层面上补全了大模型的心智拼图。
110
+
111
+ ---
112
+
113
+ ## 三、 动态插值:composeSystemPrompt() 运行时组装
114
+
115
+ 在 2.3 节我们读到,在每一次 `while(loop)` 决策环的开头,都会执行 `composeSystemPrompt()`。
116
+
117
+ 它是最终系统提示词出厂的合成车间。它会根据当前智能体正在执行的技能卡、可用的备用技能卡以及激活的工具列表,将附加信息“热拼接”到 System Prompt 的最尾部:
118
+
119
+ ```typescript
120
+ composeSystemPrompt(
121
+ activeSkill?: { id: string; content: string },
122
+ toolInstructions: string[] = [],
123
+ availableSkills: { id: string; name: string; description?: string }[] = []
124
+ ): string {
125
+ // 1. 获取包含时间戳的基础 System Prompt
126
+ let systemPrompt = this.getSystemPrompt();
127
+
128
+ // 2. 如果存在激活的工具的附加特殊说明,追加拼接入内
129
+ if (toolInstructions.length > 0) {
130
+ systemPrompt += `\n\n# TOOLS ADDITIONAL INSTRUCTIONS\n${toolInstructions.join('\n\n')}`;
131
+ }
132
+
133
+ // 3. 动态展示当前系统安装的所有可用技能卡 (并引导 LLM 具有调用 activate_skill 的概念)
134
+ if (availableSkills && availableSkills.length > 0) {
135
+ const listLines = availableSkills
136
+ .map((s) => `- **${s.name}** (技能ID: \`${s.id}\`)\n ${s.description || '无描述'}`)
137
+ .join('\n');
138
+ systemPrompt += `\n\n# AVAILABLE SKILLS (可用技能卡列表)\n本系统当前已物理安装并扫描到如下可用特长技能卡(你可通过调用 \`activate_skill("技能ID")\` 激活对应模式):\n\n${listLines}`;
139
+ }
140
+
141
+ // 4. 将当前 Session 正在处于活跃期的特定技能卡(插件)Prompt 合并入内
142
+ if (activeSkill && activeSkill.content) {
143
+ systemPrompt += `\n\n# PLUGIN PROMPT [${activeSkill.id}]\n${activeSkill.content}`;
144
+ }
145
+
146
+ return systemPrompt;
147
+ }
148
+ ```
149
+
150
+ 通过这一层拼接,大模型在每次 ReAct 推理时,其上下文的最后几行总是能接收到最新、最精准的技能和工具边界声明,从而指引其进行最理性的 Tool Call 参数预测。
151
+
152
+ 本节我们通过深入 Freya 的提示词注册表源码,白盒看清了系统提示词的物理组装线。在下一节中,我们将在本地亲自调试一个因为“占位符解析失败”导致 Agent 逻辑崩塌的经典 Bug。
@@ -0,0 +1,97 @@
1
+ ---
2
+ title: "3.4 调试与避坑指南:动态 Prompt 拼装与模板报错排查"
3
+ weight: 40
4
+ description: "实战排查提示词占位符解析失效与模板冲突,建立底座级 System Prompt 运行时捕获探针,解析长提示词的注意力衰减规避策略。"
5
+ ---
6
+
7
+ # 3.4 调试与避坑指南:动态 Prompt 拼装与模板报错排查
8
+
9
+ 在前几节中,我们确立了零硬编码的提示词分离规范,并剖析了 Freya 的双通道动态编织机制。然而,在实际的智能体开发中,动态拼装提示词就像在运行时用字符串拼接一段“SQL 语句”一样,极易发生各种微妙而致命的 Bug:
10
+ * 为什么我写的模板占位符没有被替换,大模型直接在回答里打印出了未替换的标记?
11
+ * 为什么我修改了人设提示词,Agent 的表现却没有任何变化?它收到的到底是不是我修改后的 Prompt?
12
+ * 为什么我明明在 System Prompt 开头强调了核心规则,它却依然我行我素?
13
+
14
+ 本节我们将作为 Agent 系统的调试专家,建立起运行时 Prompt 捕获探针,逐一排查并防御这些提示词织造中的物理陷阱。
15
+
16
+ ---
17
+
18
+ ## 一、 提示词编织机制与占位符解析排查
19
+
20
+ 为了将动态上下文注入到提示词中,我们通常需要理清底座的组装与替换机制,以物理防御格式损坏与逻辑失效。
21
+
22
+ ### 1. 物理拼装(零模板变量注入)
23
+ 在 Freya 底座的实际架构中,为了防止复杂的正则匹配逻辑损坏 Markdown 的语法(例如双大括号与 JSON 代码的语法冲突),**核心的 System Prompt**(如 `IDENTITY`、`SOUL`、`USER` 等)在底座中采用的是**纯 Markdown 片段拼装**的无模板机制。
24
+ 这些片段从物理 Markdown 文件中读取,并在内存注册表 `FreyaPromptRegistry` 中以段落标题的形式,通过字符串拼接直接编织成最终的 System Prompt。这种方式天然规避了占位符解析失效的风险。
25
+
26
+ ### 2. 多模态转录模板的单大括号替换
27
+ 仅在处理多模态附件(如音频文字转录、图片描述生成)时,底座在 `agent-preprocessor.ts` 中使用了简单的单大括号 `{text}` 替换机制:
28
+ ```typescript
29
+ const sttTemplate = promptRegistry.get('core.prompt.stt_template') || '{text}';
30
+ const formattedSTT = sttTemplate.replace('{text}', transcriptionText);
31
+ ```
32
+ 在自定义这些多模态描述模板时,请确保使用单大括号 `{text}`,任何拼写抖动(如误写为 `{text_content}`)都会导致大模型读到未翻译的模板占位符,从而引起格式破碎或幻觉。
33
+
34
+ ---
35
+
36
+ ## 二、 调试实战:建立运行时 System Prompt 捕获探针
37
+
38
+ 要排查拼接后的最终 Prompt,我们绝对不能闭门造车。**我们必须能够在运行时,捕获大模型在执行推理的前一微秒,收到的那个终极、完整的 System Prompt 文本。**
39
+
40
+ ### 1. 开启 Freya 物理日志捕获
41
+ 在 Freya 中,我们不需要去代码里加 `console.log`。底座在 `llm-proxy.ts` 中内建了 `FreyaLLMLogger` 机制。
42
+
43
+ 我们只需要在用户的运行时配置文件 `~/.freya/config/freya.json` 中,开启大模型日志监控:
44
+
45
+ ```json
46
+ {
47
+ "log": {
48
+ "llm": true
49
+ }
50
+ }
51
+ ```
52
+
53
+ ### 2. 观察本地捕获的 Raw 数据包
54
+ 当配置开启后,Freya 会自动在项目运行时主目录的 `logs/` 目录下(默认位于 `~/.freya/logs/`),生成按日期划分的交互日志文件,如 `llm-YYYY-MM-DD.log`。
55
+
56
+ 在此日志文件中,你可以清晰看到大模型接收到的完整 Prompt 及其结构(内容已截断序列化为 JSON 行):
57
+
58
+ ```text
59
+ [2026-08-07T13:37:10.000Z] [REQ] provider=openai model=gpt-4o msgs=1 stream=false
60
+ input=[{"role":"system","content":"# IDENTITY (本体)\n你是一个有用的助理...\n\n# SOUL (灵魂)\n你有着冷静的分析性格...\n\n# USER INFO (用户画像)\n...\n\n# CURRENT TIME (当前时间)\n2026-08-07 13:37:10 (星期五, 时区: Asia/Shanghai, UTC+8)"},{"role":"user","content":"帮我查询小明的工资。"}]
61
+ ```
62
+
63
+ 通过这一物理探针,你能一眼看出最终组装出的 System Prompt 结构是否完整、有没有拼接错误或工具定义重叠损坏。
64
+
65
+ ---
66
+
67
+ ## 三、 避坑经验:长 System Prompt 的注意力衰减防御
68
+
69
+ 在前面的架构剖析中,我们了解到大语言模型的注意力机制存在 Lost in the Middle(迷失在中间)的物理缺陷。当底座将 Identity、Soul、Memory、Tools 拼接成一个长文本时,处于中段的指令对于大模型来说注意力权重会显著衰减。
70
+
71
+ 如果大模型在推理时频繁违反你的核心约束(例如遗漏了必须要输出的固定字段),你必须巧妙利用 Freya 的物理拼接顺序来调整提示词结构。
72
+
73
+ ### 🌟 黄金法则:最致命的约束必须放在 Prompt 的尾部
74
+ 大模型在阅读文本时,对**最靠近 User 输入(即 Prompt 尾部)**的字符,其注意力点积计算得出的关联权重最大。
75
+
76
+ 在 Freya 中,底座的 `composeSystemPrompt()` 的拼接顺序为:
77
+ ```
78
+ [System Prompt 头部] -> # IDENTITY: ...
79
+ # SOUL: ...
80
+ # USER INFO: ...
81
+ [System Prompt 中段] -> # MEMORY: ...
82
+ # TOOLS SPEC: ...
83
+ # AGENT TOPOLOGY: ...
84
+ # CURRENT TIME: ...
85
+ # TOOLS ADDITIONAL INSTRUCTIONS: ...
86
+ [System Prompt 尾部] -> # AVAILABLE SKILLS: ... (可用技能卡列表)
87
+ # PLUGIN PROMPT [activeSkill.id] (当前激活的插件提示词)
88
+ [User 输入] -> "请执行任务..."
89
+ ```
90
+
91
+ * **避坑手段**:
92
+ 由于当前激活的插件提示词(`PLUGIN PROMPT`)和可用技能卡列表处于整个系统提示词的最尾端。因此,如果需要对大模型实施强力的**黄金约束或核心任务限制**,我们应当**将这些关键约束直接声明在所设计的插件提示词(`PLUGIN PROMPT`)的最底端**,或者**作为技能提示词的尾部补充**。
93
+ 这能在注意力矩阵计算时,为核心规则提供最大的物理相关度保护,彻底防止 Agent 遗忘或忽略关键约束。
94
+
95
+ 本节我们通过建立调试探针与梳理物理编织顺序,彻底锁死了提示词拼装阶段的格式 Bug 与注意力衰减漏洞。至此,我们已经成功完成了 Agent 心智模型(ReAct 决策环与提示词动态合成)的所有拼图。
96
+
97
+ 在下一部分中,我们将跨过逻辑思考的疆界,正式开启智能体掌控物理世界工具(Function Calling 原始协议与大厂流派抹平)的探索之旅。
@@ -0,0 +1,28 @@
1
+ ---
2
+ title: "第一部分:Agent 心智模型"
3
+ weight: 20
4
+ bookCollapseSection: true
5
+ ---
6
+
7
+ # 第一部分:Agent 的心智模型 —— 探秘 ReAct 决策环与沙箱物理隔离
8
+
9
+ 本部分将正式引入智能体的“心智”核心,深入解剖 ReAct 决策环,并在代码物理层面理解 Agent 是如何实现自主决策、工具调用与运行时环境隔离的。
10
+
11
+ ---
12
+
13
+ ## 🧭 章节导学与阅读清单
14
+
15
+ ### 🧠 第 2 章:智能体架构演进 —— 从聊天助手到能动的主体
16
+ 剖析 ReAct 主决策环 `while(loop)` 运行逻辑,解密智能体如何通过 Reasoning 和 Acting 形成决策闭环,并分析 ReAct 自循环失控的成因。
17
+ * 👉 **[2.1 能动性(Agency)的诞生](2.1_agency_vs_chatbot.md)**
18
+ * 👉 **[2.2 ReAct 心智模型与推演](2.2_react_mind_model.md)**
19
+ * 👉 **[2.3 【白盒剖析】决策循环与 agent-executor 源码](2.3_freya_agent_executor.md)**
20
+ * 👉 **[2.4 调试与避坑指南:ReAct 自循环失控熔断](2.4_debugging_loop_deadlock.md)**
21
+
22
+ ### 📝 第 3 章:系统提示词的动态织造与零硬编码规范
23
+ 探讨如何通过物理分离和热加载避免在 TypeScript 源码中硬编码提示词,并解密运行时双通道动态合并机制。
24
+ * 👉 **[3.1 提示词工程的痛点](3.1_hardcoded_prompt_pain.md)**
25
+ * 👉 **[3.2 零硬编码解耦架构](3.2_decoupled_architecture.md)**
26
+ * 👉 **[3.3 【白盒剖析】双通道动态提示词合并](3.3_freya_dual_read_probe.md)**
27
+ * 👉 **[3.4 调试与避坑指南:动态 Prompt 拼装与模板报错排查](3.4_debugging_composition_placeholder.md)**
28
+
@@ -0,0 +1,119 @@
1
+ ---
2
+ title: "4.1 语言到动作的转换"
3
+ weight: 10
4
+ description: "揭密大模型如何通过 JSON Schema 感知外部工具,解析参数自动推导与受约束生成(Constrained Decoding)的底层原理。"
5
+ ---
6
+
7
+ # 4.1 语言到动作的转换
8
+
9
+ 大语言模型(LLM)天然只是一个在概率空间里进行“文字接龙”的文本发生器。它所处理和吐出的都是**非结构化**的自然语言。
10
+
11
+ 然而,物理世界里的计算机软件、数据库以及各种网络 API,是极度冰冷和严谨的。它们要求输入的数据必须是 100% **强类型、结构化**的参数(例如:一个读取文件接口要求传入 `{"path": "notes.txt", "startLine": 1, "endLine": 10}`,不能多一个逗号,也不能把整数 `1` 传成字符串 `"one"`)。
12
+
13
+ 大模型是如何将用户模糊的自然语言(如“查看 notes.txt 文件的前 10 行”),瞬间转化为完美的、计算机可读取的强类型 API 调用参数的?
14
+
15
+ 这中间的物理桥梁,就是 **JSON Schema 参数描述符**。
16
+
17
+ ---
18
+
19
+ ## 一、 JSON Schema:工具的物理“产品说明书”
20
+
21
+ 在给大模型挂载工具(Tool)时,底座系统必须向大模型发送一份关于这些工具的**声明描述**。
22
+
23
+ 大模型目前最能理解的声明规范,就是由 IETF 制定的 **JSON Schema**。它是一种基于 JSON 的元数据声明,用来定义和校验 JSON 数据的结构。
24
+
25
+ ### 1. 一个标准的工具 Schema 物理标本:
26
+ ```json
27
+ {
28
+ "name": "read_file",
29
+ "description": "读取指定文件的文本内容。支持指定行号起止区间切片读取,防范大文件上下文超限。",
30
+ "parameters": {
31
+ "type": "object",
32
+ "properties": {
33
+ "path": {
34
+ "type": "string",
35
+ "description": "目标文件的相对路径(如 'notes.txt')"
36
+ },
37
+ "scope": {
38
+ "type": "string",
39
+ "enum": ["workspace", "src", "doc"],
40
+ "description": "读取作用域(可选,默认为 'workspace' 沙箱,支持指定 'src' 源码区或 'doc' 文档区)"
41
+ },
42
+ "startLine": {
43
+ "type": "integer",
44
+ "description": "起始行号(可选,1-indexed,包含该行。默认为第 1 行)"
45
+ },
46
+ "endLine": {
47
+ "type": "integer",
48
+ "description": "结束行号(可选,1-indexed,包含该行。默认读取至文件末尾)"
49
+ }
50
+ },
51
+ "required": ["path"]
52
+ }
53
+ }
54
+ ```
55
+
56
+ ### 2. 大模型如何阅读这份说明书?
57
+ 在进行 SFT(监督微调)时,现代大模型(尤其是具备 Tool Call 能力的模型)的训练集中包含了海量的 JSON Schema 与对应参数提取的数据对。
58
+
59
+ 当大模型接收到上面的 Schema 并结合用户的提问 “*查看 workspace 作用域下 notes.txt 文件的前 10 行*” 进行注意力点积计算时:
60
+ * **语义对齐(Semantic Alignment)**:模型的自注意力矩阵会将用户输入中的“notes.txt”与 `properties.path` 相关联,将“workspace”与 `properties.scope` 里的枚举值关联。
61
+ * **类型提取(Entity Extraction)**:注意力层会捕获“前 10 行”,并将其映射到 `properties.startLine` 的默认值 `1`,以及 `properties.endLine` 的类型定义 `integer`,并将其翻译为数学数字 `10`。
62
+ * **必填校验(Required Check)**:模型会核对 `required` 列表,确保生成时包含必填参数 `path`。
63
+
64
+ ---
65
+
66
+ ## 二、 受约束生成 (Constrained Decoding) 的底层控制
67
+
68
+ 很多人以为大模型输出工具调用参数,是它自己在脑子里写出了 JSON。但在推理引擎的物理层面,这是通过 **受约束生成(Constrained Decoding)** 强行干预 Token 采样概率实现的。
69
+
70
+ ### 1. Logit Bias 与语法引导
71
+ 当大模型阅读了上面的 `read_file` Schema,并决定在接龙中调用这个工具时,底座推理引擎(如 vLLM 或 llama.cpp)会激活 **CFG(上下文无关语法)约束器**。
72
+ * 约束器会根据 JSON Schema 的定义,在每一步 Token 预测时,强行将那些不符合 JSON 语法和 Schema 字段要求的 Token 概率(Logits)**归零**。
73
+ * 例如,在输出完 `"scope": "` 之后,下一个 Token 的可选概率池会被强制收缩为“只能是双引号包围的 `'workspace'`、`'src'` 或 `'doc'` 之一”,代表其他所有控制符和无关单词的概率被瞬间物理剥夺。
74
+
75
+ ```
76
+ [大模型 Logits 输出] ───> {"workspace": 60%, "root": 30%, "all": 10%}
77
+
78
+ ▼ (语法约束器介入:根据 scope 的 enum 限制,非合法枚举值概率归零)
79
+ [修正后 Logits 分布] ───> {"workspace": 100%, "root": 0%, "all": 0%} ──> 强制生成合规参数值
80
+ ```
81
+
82
+ 通过这层底座级的受约束生成控制,模型输出的 JSON 参数具有极高的格式合规率,消除了因为多一个空格或少一个逗号导致解析失败的隐患。
83
+
84
+ ---
85
+
86
+ ## 三、 【避坑指南】高质量 Schema Description 编写准则
87
+
88
+ 在实际开发中,智能体之所以会传错参数或者凭空编造不存在的参数,**90% 的原因都是因为开发者偷懒,把 Schema 的 `description` 写得极其简陋。**
89
+
90
+ 大模型在阅读 Schema 时,极度依赖 `description` 里的自然语言指示。
91
+
92
+ ### ❌ 简陋的反面教材:
93
+ ```json
94
+ "properties": {
95
+ "path": {
96
+ "type": "string",
97
+ "description": "文件路径"
98
+ }
99
+ }
100
+ ```
101
+ * *后果*:当用户输入“帮我看一下系统配置”,大模型因为不知道该工具仅支持工作区内的相对路径,可能会脑补生成物理绝对路径 `{"path": "/etc/hosts"}`。但后端接口出于安全隔离要求只允许相对路径,最终会导致插件执行报错(甚至抛出安全越界异常)。
102
+
103
+ ### 🌟 黄金规范的正面教材:
104
+ ```json
105
+ "properties": {
106
+ "path": {
107
+ "type": "string",
108
+ "description": "目标文件的相对路径(如 'notes.txt')。安全防范:绝对禁止传入以 '/' 或 'C:\\' 开头的物理绝对路径,仅允许使用以当前工作区根目录为基准的相对路径。"
109
+ }
110
+ }
111
+ ```
112
+
113
+ ### 编写 Schema 的四大防御性原则:
114
+ 1. **明确格式约束**:如果参数有格式要求,必须在 `description` 中给出正反例(如:“相对路径,必须以当前目录为基准,例如 'src/index.ts',绝对不能传入 '/src/index.ts'”)。
115
+ 2. **明确空值行为**:明确告诉模型在参数缺失时应该填入什么默认值,或者是否应该置空(如:“如果用户未提及具体起止行,`startLine` 和 `endLine` 必须置空不传,绝对不能臆测具体行号”)。
116
+ 3. **声明前置依赖关系**:如果参数 A 依赖参数 B,在 A 的描述里写清依赖机制,引导模型先去执行前置工具(如:“在读取文件前,若无法确认其相对路径,请引导大模型先调用 `list_dir` 获取目录结构”)。
117
+ 4. **限制参数枚举值**:尽可能使用 `enum` 限制取值范围(如 `"enum": ["workspace", "src", "doc"]`),防止大模型自由发挥输出不支持的参数选项(如输出 `root` 等无效作用域)。
118
+
119
+ 通过本节的解剖,我们明白了自然语言转化成结构化 API 的本质是 **JSON Schema 注意力对齐** 与 **受约束接龙** 的物理结合。在下一节中,我们将实际抓取大模型在触发工具调用时吐出的 Raw 网络数据包,看看它在网络传输中的最原始样貌。
@@ -0,0 +1,105 @@
1
+ ---
2
+ title: "4.2 【白盒剖析】Tool Call Raw 数据包结构"
3
+ weight: 20
4
+ description: "剖析大模型触发 tool_calls 的网络原始 JSON 数据包,探究非流式与流式数据结构的差异,以及 Freya 的非流式工程处理设计。"
5
+ ---
6
+
7
+ # 4.2 【白盒剖析】Tool Call Raw 数据包结构
8
+
9
+ 在 4.1 节中,我们了解了大模型如何通过 JSON Schema 规则来理解工具的输入约束。那么,当大模型在接龙过程中决定“动手”调用这些工具时,它发回给我们的网络原始数据包到底长什么样?
10
+
11
+ 本节我们将捕获并解剖大模型触发 `tool_calls` 时的网络 Raw 原始 JSON 数据结构,了解底座接口是如何安全提取大模型意图的,并揭示 Freya 在处理工具调用时的工程简化艺术。
12
+
13
+ ---
14
+
15
+ ## 一、 非流式响应:大体量 JSON 标本
16
+
17
+ 当大模型在非流式模式下决定调用工具时,底座接收到的 HTTP Response 响应体是一个完整的 JSON 包。
18
+
19
+ 以下是一个真实的非流式 Tool Call 响应标本(基于真实的 `read_file` 工具):
20
+
21
+ ```json
22
+ {
23
+ "id": "chatcmpl-9B8t5z...",
24
+ "object": "chat.completion",
25
+ "created": 1711239999,
26
+ "model": "gpt-4o",
27
+ "choices": [
28
+ {
29
+ "index": 0,
30
+ "message": {
31
+ "role": "assistant",
32
+ "content": null,
33
+ "tool_calls": [
34
+ {
35
+ "id": "call_file_01",
36
+ "type": "function",
37
+ "function": {
38
+ "name": "read_file",
39
+ "arguments": "{\"path\":\"notes.txt\",\"scope\":\"workspace\",\"startLine\":1,\"endLine\":10}"
40
+ }
41
+ }
42
+ ]
43
+ },
44
+ "finish_reason": "tool_calls"
45
+ }
46
+ ]
47
+ }
48
+ ```
49
+
50
+ ### 关键字段的物理拆解:
51
+ 1. `finish_reason: "tool_calls"`:这是底座最先捕获的物理路标。它明确告诉底座执行器:“大模型推理已经暂停,当前是因为需要执行工具而退出的”。
52
+ 2. `tool_calls[0].id`(如 `call_file_01`):由大模型服务器随机生成的全局唯一调用 ID。底座稍后执行完工具后,回传的 `role: "tool"` 消息必须携带此 ID,否则大模型将无法把执行结果与这一步推理正确匹配。
53
+ 3. `tool_calls[0].function.arguments`:**这是整个协议中最重要的工程秘密**。注意看,它的值是一个**被序列化为字符串的 JSON 文本**,而不是嵌套的 JSON 对象。
54
+ * *成因*:因为大模型本质上是一个“文字接龙器”,它输出的一切内容在物理上都是连续的文本字符。因此,它只能接龙生成一串符合 JSON 格式的字符串。底座在获取该字段后,**必须手动在本地调用 `JSON.parse()` 将其反序列化为对象**,方可传给真实的本地函数。
55
+
56
+ > [!NOTE]
57
+ > **🎨 Freya 的非流式处理设计哲学**
58
+ > 虽然大模型流式(Stream)输出在生成普通文本时体验极佳,但在流式输出工具调用(Tool Call)时,由于参数是一串连续生成的字符流,底座接收端若采用流式解析则需要应对繁杂的分段拼接与并行交错处理。
59
+ >
60
+ > 因此,基于系统与教程简化的需要,Freya 源码(如 `plugins/plugin-openai/src/index.ts` 和 `plugins/plugin-gemini/src/index.ts`)直接采用了更简单、直观的非流式模式处理:
61
+ > ```typescript
62
+ > // 只要挂载了外部工具 (tools.length > 0),就强制关闭流式传输
63
+ > const isStream = !!options?.onChunk && (!tools || tools.length === 0);
64
+ > ```
65
+ > 通过这种设计,系统可以直接以完整的非流式 JSON 数据包获取全部的 `tool_calls` 参数,省去了繁杂的流式增量拼接代码,用最精简的开发代价换取了极佳的可读性与稳定性。
66
+
67
+ ---
68
+
69
+ ## 二、 进阶原理:流式响应下的参数分段与拼接
70
+
71
+ 如果在其他开源底座框架中开启了流式 Tool Call 支持,网络数据包的传输将采用 SSE(服务器发送事件)分包推送。
72
+
73
+ 在流式状态下,大模型并不是一次性返回 `arguments` 字符串,而是以增量片段(Delta)的形式,像“挤牙膏”一样连续输出:
74
+ * **物理包 1**:声明准备调用的函数名 `read_file` 以及随机生成的 ID。
75
+ * **物理包 2**:传输参数的开头字符,如 `"{"pa"`。
76
+ * **物理包 3**:传输参数的中间部分,如 `"th":"notes.txt"`。
77
+ * **物理包 4**:传输参数的结尾及结束标识符 `"}"`。
78
+
79
+ 此时,底座在接收到流式 Delta 时,必须在内存中开辟临时缓冲区进行**增量拼接(Delta Assembly)**,把这些碎片字符拼成合法的 JSON 字符串,直至捕获到 `finish_reason: "tool_calls"` 时,再调用 `JSON.parse(buffer)` 来实例化对象。
80
+
81
+ ---
82
+
83
+ ## 三、 进阶原理:并行工具调用(Parallel Tool Calls)的交错网络流
84
+
85
+ 现代大模型(如 GPT-4o)具备强大的**并行工具调用**能力。例如,当用户下达指令“*查看 notes.txt 与 config.json 这两个文件*”时,大模型会在单次网络响应中同时触发两个 `read_file` 的意图。
86
+
87
+ ### 1. 并行流的交错返回现象
88
+ 大模型在向客户端回传流时,并不会等待 `index: 0` 输出完毕再输出 `index: 1`,而是将两者混杂在一起分段推送:
89
+ ```
90
+ [Chunk A] ──> index: 0, delta: "{"path":"notes"
91
+ [Chunk B] ──> index: 1, delta: "{"path":"config"
92
+ [Chunk C] ──> index: 0, delta: ".txt"}"
93
+ [Chunk D] ──> index: 1, delta: ".json"}"
94
+ ```
95
+
96
+ ### 2. 多缓冲区隔离累加
97
+ 为了防止不同流的数据被强行交叉混淆拼接,底座必须使用一个 Map 结构,以返回包中的 `tool_calls[i].index` 索引作为 Key,为每一个并行工具调用分配一个独立的参数缓冲区。
98
+
99
+ ```
100
+ index 0 Buffer ──> 累加得到 ──> {"path":"notes.txt"} ──> 解析执行
101
+ index 1 Buffer ──> 累加得到 ──> {"path":"config.json"} ──> 解析执行
102
+ ```
103
+
104
+ **总结**:
105
+ 通过本节的学习,我们理解了 LLM 工具调用的协议数据本质是**序列化的 JSON 文本**。虽然流式/并行拼接在协议层十分优美,但基于教程简化的需要,系统直接采用了非流式模式处理,避开了这种高复杂度的拼接处理。在下一小节中,我们将看看底座拿到这些完整的 JSON 参数后,是如何在本地建立起安全隔离的沙箱环境来执行工具的。
@@ -0,0 +1,145 @@
1
+ ---
2
+ title: "4.3 【白盒剖析】工具安全防线与本地执行"
3
+ weight: 30
4
+ description: "白盒解剖 Freya 的 tool-registry.ts 源码与执行器工具调度链,探秘元工具箱设计、授权安全隔离与 EventBus 状态追踪机制。"
5
+ ---
6
+
7
+ # 4.3 【白盒剖析】工具安全防线与本地执行
8
+
9
+ 在 4.2 节中,我们抓包剖析了流式与非流式下大模型返回的 `tool_calls` 网络 Raw 数据包。当我们的智能体底座在网络接收层完整拼接并反序列化出参数后,接下来就必须进入最关键的物理环节 —— **在本地服务器上查找、校验并安全执行对应的工具函数。**
10
+
11
+ 在这个阶段,底座必须建立起坚实的安全防线。因为大模型具有“幻觉”和“过度自信”的物理缺陷,它可能会凭空编造出一个未获授权的工具名称,或者试图越权调用其他用户的私有工具。
12
+
13
+ 本节我们将对照阅读 Freya 的核心工具注册表(位于 `packages/core/src/tools/tool-registry.ts`)与执行器调度链的源码,解密底座是如何建立起“工具沙箱安全隔离区(Tool Sandbox)”的。
14
+
15
+ ---
16
+
17
+ ## 一、 内核工具注册表 FreyaToolRegistry 的设计
18
+
19
+ 在 `packages/core/src/tools/tool-registry.ts` 中,`FreyaToolRegistry` 负责聚合系统内部和所有插件挂载的外部工具:
20
+
21
+ ```typescript
22
+ export class FreyaToolRegistry {
23
+ // 聚合所有已注册的工具箱
24
+ private toolboxes: FreyaToolbox[] = [];
25
+
26
+ /** 注册一个工具箱(支持插件体系的热插拔覆盖) */
27
+ registerToolbox(toolbox: FreyaToolbox): void {
28
+ const newId = toolbox.getId();
29
+ const existingIndex = this.toolboxes.findIndex((tb) => tb.getId() === newId || tb === toolbox);
30
+
31
+ if (existingIndex > -1) {
32
+ this.toolboxes[existingIndex] = toolbox;
33
+ } else {
34
+ this.toolboxes.push(toolbox);
35
+ }
36
+ }
37
+ }
38
+ ```
39
+
40
+ ### 1. 声明式解耦:工具箱(Toolbox)的聚合
41
+ 底座并不以“单个原子函数”为单位进行管理,而是将工具成套组织在**工具箱(Toolbox)**中。
42
+ 每个工具箱拥有唯一的 ID 契约。插件只需要实现 SDK 中的 `FreyaToolbox` 接口,即可声明式地将自己的工具集热插拔式地挂载到内核中。
43
+
44
+ ---
45
+
46
+ ## 二、 动态过滤:工具授权沙箱的物理防线
47
+
48
+ 大模型的一大典型 bug 叫做“记忆越权”。如果一个模型在前面的对话中被挂载了敏感的 `admin_delete_user` 工具,在换入普通用户 Session 后,即便我们在 System Prompt 中抹去了该工具的声明,**大模型的大脑权重中可能依然残存着该工具的调用概率**。大模型有可能会直接输出 `Action: admin_delete_user(...)`。
49
+
50
+ 为了在物理上彻底堵死这一后门,Freya 引入了**动态过滤防线**:
51
+
52
+ ```typescript
53
+ /**
54
+ * 根据当前会话已激活的工具箱列表,动态过滤获取所需的工具字典
55
+ */
56
+ getFilteredTools(activeToolboxIds: string[]): Map<string, FreyaTool> {
57
+ const activeSet = new Set(activeToolboxIds || []);
58
+ const tools = new Map<string, FreyaTool>();
59
+
60
+ for (const toolbox of this.toolboxes) {
61
+ const toolboxId = toolbox.getId();
62
+
63
+ // 💡 物理铁律:ID 为 'meta' 的工具箱是内置元工具箱,无条件默认开启;
64
+ // 其他插件工具箱必须显式存在于当前 Session 的激活列表中才会暴露
65
+ if (toolboxId === 'meta' || activeSet.has(toolboxId)) {
66
+ for (const tool of toolbox.getTools()) {
67
+ tools.set(tool.getDefinition().name, tool);
68
+ }
69
+ }
70
+ }
71
+ return tools;
72
+ }
73
+ ```
74
+
75
+ 在执行器 `agent-executor.ts` 中,ReAct 循环每次只会从 `getFilteredTools` 返回的这个**经过物理裁剪过滤**的 `tools` 字典中去检索工具:
76
+ ```typescript
77
+ const tool = tools.get(toolCall.name);
78
+ ```
79
+
80
+ ### 物理安全效果:
81
+ 这意味着,**哪怕大模型通过幻觉或者越狱攻击预测出了 `admin_delete_user` 的名字,由于该工具箱不在当前 Session 的 `activeToolboxIds` 中,底座在检索 `tools.get()` 时只会返回 `undefined`,从而被物理拦截!**
82
+
83
+ 这在进程运行时层面建立了一个无法被越权的“工具物理沙箱(Tool Sandbox)”。
84
+
85
+ ---
86
+
87
+ ## 三、 异步工具调度与 EventBus 状态流转
88
+
89
+ 当底座通过安全校验后,执行器会启动并发 Promise 链来执行真正的本地 JS/TS 工具代码。
90
+
91
+ 为了让前台 UI 或调试控制台能够实时跟踪工具的进度(而不至于让用户盯着一个转圈的 loading 发呆),Freya 设计了基于 **EventBus** 的异步状态流转:
92
+
93
+ ```typescript
94
+ // 1. 广播工具开始执行事件,透传 arguments 方便前台打印
95
+ this.context.eventBus.emit('tool:status', {
96
+ sessionId,
97
+ toolCallId: toolCall.id,
98
+ toolName: toolCall.name,
99
+ status: 'running',
100
+ arguments: args
101
+ });
102
+
103
+ try {
104
+ // 2. 在独立的 async 栈中执行真实的工具逻辑,注入 context 句柄
105
+ const result = await tool.execute(args, this.context);
106
+
107
+ // 3. 执行成功,广播 completed 并返回 Observation 文本
108
+ this.context.eventBus.emit('tool:status', {
109
+ sessionId,
110
+ toolCallId: toolCall.id,
111
+ toolName: toolCall.name,
112
+ status: 'completed',
113
+ arguments: args,
114
+ result
115
+ });
116
+ return {
117
+ role: 'tool' as const,
118
+ content: result,
119
+ toolCallId: toolCall.id,
120
+ toolName: toolCall.name
121
+ };
122
+ } catch (err: any) {
123
+ // 4. 捕获报错,广播 failed,并将报错文本作为 Observation 返回,引导 LLM 纠错
124
+ this.context.eventBus.emit('tool:status', {
125
+ sessionId,
126
+ toolCallId: toolCall.id,
127
+ toolName: toolCall.name,
128
+ status: 'failed',
129
+ arguments: args,
130
+ result: err.message
131
+ });
132
+ return {
133
+ role: 'tool' as const,
134
+ content: `Error during execution: ${err.message}`, // 错误回流引导
135
+ toolCallId: toolCall.id,
136
+ toolName: toolCall.name
137
+ };
138
+ }
139
+ ```
140
+
141
+ ### 物理设计优势:
142
+ 1. **容错回流设计**:在 catch 块中,底座发生工具执行错误时**绝不崩溃**,而是将报错信息规整为标准的 `{ role: "tool", content: "Error: ..." }` 回传给大模型。这让大模型能够在下一轮迭代中阅读到错误堆栈并尝试“换个参数重新调用”。
143
+ 2. **解耦进度监听**:通过 EventBus 广播的 `tool:status` 状态流(`running` -> `completed`/`failed`),可以让独立的 WebSocket 插件随时向 Web 前端渲染出精美的“智能体正在查询数据库...”等微步进度条,极大地提升了用户交互心流体验。
144
+
145
+ 本节我们通过阅读注册表和执行器源码,看清了工具本地执行时的安全隔离防线与状态通知机制。在下一节中,我们将亲自在本地进行调试,去挽救由于“Observation 返回格式破坏”导致的智能体决策坍塌。