@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.
- package/README.md +9 -3
- package/core/dist/command/commands/skill-commands.js +5 -4
- package/core/dist/config/config-manager.d.ts +5 -1
- package/core/dist/config/config-manager.js +13 -1
- package/core/dist/kernel.js +2 -2
- package/core/dist/skill/skill-registry.d.ts +12 -2
- package/core/dist/skill/skill-registry.js +89 -8
- package/core/dist/tools/meta/index.d.ts +3 -1
- package/core/dist/tools/meta/index.js +9 -1
- package/core/dist/web/config-api.js +18 -0
- 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 +3 -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-mysql/config/prompts/plugin.prompt.mysql.md +9 -0
- package/plugins/plugin-tool-mysql/config/prompts/plugin.prompt.mysql.select.audit.md +26 -0
- package/plugins/plugin-tool-mysql/dist/audit.d.ts +13 -0
- package/plugins/plugin-tool-mysql/dist/audit.js +87 -0
- package/plugins/plugin-tool-mysql/dist/index.d.ts +14 -0
- package/plugins/plugin-tool-mysql/dist/index.js +34 -0
- package/plugins/plugin-tool-mysql/dist/pool-manager.d.ts +32 -0
- package/plugins/plugin-tool-mysql/dist/pool-manager.js +113 -0
- package/plugins/plugin-tool-mysql/dist/tools.d.ts +11 -0
- package/plugins/plugin-tool-mysql/dist/tools.js +88 -0
- package/plugins/plugin-tool-mysql/package.json +33 -0
- package/plugins/plugin-tool-mysql/schema.json +83 -0
- 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
- package/src/packages/core/src/command/commands/skill-commands.ts +6 -5
- package/src/packages/core/src/config/config-manager.ts +15 -1
- package/src/packages/core/src/kernel.ts +3 -2
- package/src/packages/core/src/skill/skill-registry.ts +100 -8
- package/src/packages/core/src/tools/meta/index.ts +9 -1
- package/src/packages/core/src/web/config-api.ts +20 -0
- package/src/packages/ui/src/features/config/ConfigModal.tsx +13 -1
- package/src/packages/ui/src/features/config/panels/SkillConfigPanel.tsx +137 -0
- package/src/plugins/plugin-tool-mysql/src/audit.ts +103 -0
- package/src/plugins/plugin-tool-mysql/src/index.ts +44 -0
- package/src/plugins/plugin-tool-mysql/src/pool-manager.ts +132 -0
- package/src/plugins/plugin-tool-mysql/src/tools.ts +100 -0
- package/ui/assets/{index-Be0cAgdB.js → index-BqPQMflk.js} +14 -14
- package/ui/index.html +1 -1
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "4.4 调试与避坑指南:错误 Observation 与自我修正"
|
|
3
|
+
weight: 40
|
|
4
|
+
description: "实战调试工具执行参数缺失与格式不符,设计引导性错误 Observation 机制,防范海量返回数据导致的上下文崩溃。"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 4.4 调试与避坑指南:错误 Observation 与自我修正
|
|
8
|
+
|
|
9
|
+
在前几节中,我们解剖了工具调用的 JSON Schema 规范、Raw 数据包流式拼接,以及 Freya 底座的工具沙箱物理隔离机制。
|
|
10
|
+
|
|
11
|
+
在真实的 Agent 工程实践中,工具调用链路中最容易发生崩溃的其实是**工具执行结果带回大模型的那一瞬间**。如果工具执行失败,或者带回的 Observation(观察事实)格式受到污染,会导致大模型大脑发生灾难性的**注意力坍塌**:
|
|
12
|
+
* 工具抛出数据库报错,大模型直接被底层报错堆栈(如 TCP Conn Reset)淹没,完全丧失了接下来继续规划的能力。
|
|
13
|
+
* 工具参数传错(如必填项缺失),如果不加引导,大模型会陷入“重复调用 -> 重复报错”的死循环。
|
|
14
|
+
* 爬虫工具带回了 20 万字的 HTML 垃圾网页字符,瞬间撑爆 Context Window,直接导致 API 请求被网关拒绝。
|
|
15
|
+
|
|
16
|
+
本节我们将实际编写引导性的错误 Observation,并设计大体量 Observation 截断防御,解决工具交互中的致命物理坑。
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 一、 引导性错误 Observation:教大模型“自我纠错”
|
|
21
|
+
|
|
22
|
+
当大模型生成的参数不符合 JSON Schema(例如缺失必填项,或参数类型错误)时,底座执行器如果只是呆板地返回 `Error: Bad parameter`,大模型在阅读后很难知道自己错在哪里,多半会在下一轮中重复犯错。
|
|
23
|
+
|
|
24
|
+
### 1. 什么是引导性错误反馈?
|
|
25
|
+
引导性错误反馈的原则是:**在底座拦截校验错误,并将错误详细分类,用极其明确的格式指出“哪个参数错了,期望什么格式,你应该怎么修正”**。
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
[LLM 传入错误参数] ───> 【底座校验拦截】 ───> 织造引导性 Observation ───> [LLM 阅读后瞬间醒悟并自动修正]
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### 2. 本地调试实战代码
|
|
32
|
+
让我们在本地模拟一个带 Schema 自动校验和引导性纠错的执行片段:
|
|
33
|
+
|
|
34
|
+
```javascript
|
|
35
|
+
// 模拟的工具配置
|
|
36
|
+
const tools = {
|
|
37
|
+
get_user_profile: {
|
|
38
|
+
required: ["userId"],
|
|
39
|
+
execute: async (args) => {
|
|
40
|
+
// 1. 物理拦截校验
|
|
41
|
+
if (!args.userId) {
|
|
42
|
+
throw new Error('Parameter "userId" is missing. Please note that "userId" must be a 10-digit number (e.g. 2026001001). Do not pass name or username.');
|
|
43
|
+
}
|
|
44
|
+
return JSON.stringify({ name: "小明", age: 18 });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// 模拟底座的 ReAct 纠错环
|
|
50
|
+
async function runCorrectionLoop() {
|
|
51
|
+
const history = [];
|
|
52
|
+
|
|
53
|
+
// 第一轮:大模型自作聪明,把 userId 传成了 userName
|
|
54
|
+
const firstLlmResponse = {
|
|
55
|
+
toolCalls: [{ id: "call_01", name: "get_user_profile", arguments: '{"userName":"小明"}' }]
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
console.log("🤖 [LLM]: 我决定调用 get_user_profile,参数为:", firstLlmResponse.toolCalls[0].arguments);
|
|
59
|
+
history.push({ role: "assistant", toolCalls: firstLlmResponse.toolCalls });
|
|
60
|
+
|
|
61
|
+
const tc = firstLlmResponse.toolCalls[0];
|
|
62
|
+
const tool = tools[tc.name];
|
|
63
|
+
const args = JSON.parse(tc.arguments);
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const result = await tool.execute(args);
|
|
67
|
+
history.push({ role: "tool", content: result, toolCallId: tc.id });
|
|
68
|
+
} catch (err) {
|
|
69
|
+
// 💡 物理回流:将带格式化引导的友好报错灌回
|
|
70
|
+
const friendlyError = `Error during calling [${tc.name}]: ${err.message}`;
|
|
71
|
+
console.log(`⚙️ [底座 Observation 引导注入]:\n${friendlyError}`);
|
|
72
|
+
history.push({ role: "tool", content: friendlyError, toolCallId: tc.id });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 模拟第二轮:LLM 阅读了引导性 Observation 后再次决策
|
|
76
|
+
// 注意力机制会精确定位到 "Parameter userId is missing... Do not pass name"
|
|
77
|
+
console.log("\n--- 大模型阅读 Observation 后进行第二轮决策 ---");
|
|
78
|
+
const secondLlmResponse = {
|
|
79
|
+
toolCalls: [{ id: "call_02", name: "get_user_profile", arguments: '{"userId":2026001001}' }]
|
|
80
|
+
};
|
|
81
|
+
console.log("🤖 [LLM 自我修正]: 我收到了刚才的报错。我这次将使用正确的 userId 参数:", secondLlmResponse.toolCalls[0].arguments);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
runCorrectionLoop();
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
通过这一层引导性 Observation 设计,原本可能发生崩溃的 Agent,在第二轮中完成了极其优雅的**自适应自我修正**。
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## 二、 垃圾信息过滤:防范报错堆栈污染
|
|
92
|
+
|
|
93
|
+
在大模型与外部接口进行物理通信时,接口抛出的 Raw Exception(如 Node.js 或 Java 抛出的底层 50 行 Call Stack)包含大量的内存指针、物理文件路径和网络握手日志。
|
|
94
|
+
* **严重隐患**:这些纯技术堆栈会让大模型“不知所措”。由于 Attention 权重的限制,它会把宝贵的注意力分配给报错堆栈里的文件路径或内存地址,从而完全偏离了用户的业务目标。
|
|
95
|
+
* **架构防范法则**:底座必须对捕获的所有 `Error` 对象进行**指纹裁剪**。只保留 `error.message` 的首行摘要,强行剥离所有物理路径和调用栈(Call Stack),再作为 Observation 吐回给大模型。
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## 三、 海量 Observation 的物理防线:沙箱落盘与带引导的截断
|
|
100
|
+
|
|
101
|
+
在开发如“网页爬虫”或“接口大响应数据获取”工具时,外部接口常常会带回几万乃至几十万字的文本或庞大的 HTML/JSON 原始串。如果无节制地直接塞入消息历史:
|
|
102
|
+
* **后果**:超出了 Context Window,下一轮 LLM 请求会直接被云端网关抛出 `context_length_exceeded` 拒绝。
|
|
103
|
+
|
|
104
|
+
在 Freya 中,底座在工具插件层(以 `plugin-tool-web` 为例)建立了两道物理安全防线:
|
|
105
|
+
|
|
106
|
+
### 1. 防线一:大响应与二进制自动落盘沙箱
|
|
107
|
+
当网页内容或接口响应体积超过预设阈值(或为二进制/PDF/图片文件)时,工具不直接内联返回全文,而是自动将原始内容持久化到隔离工作区的 `download/` 目录下,仅向大模型返回文件存储路径,并引导其使用分段读取工具:
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
// 摘自 plugins/plugin-tool-web/src/tools.ts
|
|
111
|
+
if (shouldAutoSave(args, responseText.length, responseContentType, autoSaveThreshold)) {
|
|
112
|
+
const relPath = await saveToWorkspace(
|
|
113
|
+
workspaceDir,
|
|
114
|
+
url,
|
|
115
|
+
responseText,
|
|
116
|
+
responseContentType
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const lines: string[] = [];
|
|
120
|
+
lines.push(`HTTP ${response.status} ${response.statusText} — ${url}`);
|
|
121
|
+
lines.push(`已自动保存到工作区文件: ${relPath}`);
|
|
122
|
+
lines.push('');
|
|
123
|
+
lines.push('可通过 read_file 工具分段读取该文件进行分析。');
|
|
124
|
+
return lines.join('\n');
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### 2. 防线二:内容净化与带引导参数的字符截断
|
|
129
|
+
当响应允许内联返回时,先进行 HTML 文本净化(剥离无用标签),再通过字符截断函数 `truncateContent` 控制长度,并在尾部追加明确的引导信息:
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
// 摘自 plugins/plugin-tool-web/src/utils.ts
|
|
133
|
+
export function truncateContent(content: string, maxLength?: number): string {
|
|
134
|
+
const limit = maxLength && maxLength > 0 ? maxLength : DEFAULT_MAX_LENGTH;
|
|
135
|
+
|
|
136
|
+
if (content.length <= limit) {
|
|
137
|
+
return content;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const truncated = content.slice(0, limit);
|
|
141
|
+
return (
|
|
142
|
+
truncated +
|
|
143
|
+
`\n\n... [响应内容已截断,原始长度 ${content.length} 字符,当前限制 ${limit} 字符。可通过 maxLength 参数调整限制]`
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### 物理设计优势:
|
|
149
|
+
在截断末尾追加 `[响应内容已截断...可通过 maxLength 参数调整限制]` 的好处在于:
|
|
150
|
+
1. **防止片面认知**:大模型阅读到截断提示后,在下一轮思考(Thought)中能明确知晓“当前获取的信息不完整”,防止得出断章取义的错误结论。
|
|
151
|
+
2. **赋予自主调节能力**:大模型可根据实际需要,在下一次调用时主动传入更大的 `maxLength` 参数,或者改用文件读取工具分段阅读。
|
|
152
|
+
|
|
153
|
+
通过本节对 Observation 的引导纠错治理、垃圾堆栈过滤和工具级沙箱截断防线的学习,我们彻底确保了智能体在物理工具执行层面的高鲁棒性。在下一章中,我们将进入多轮关联的流派,去抹平 OpenAI 与 Gemini 之间最大的 Function Calling 协议差异。
|
|
154
|
+
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "5.1 工具执行结果的归流"
|
|
3
|
+
weight: 10
|
|
4
|
+
description: "解密多轮工具交互时的 Observation 反向注入机制,剖析完整的物理消息交替流转与协议完整性红线。"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 5.1 工具执行结果的归流
|
|
8
|
+
|
|
9
|
+
在第 4 章中,我们深入了解了智能体如何通过底座执行本地工具代码并获取执行结果。然而,仅仅拿到工具执行后的文本或 JSON 结果还不够。我们必须将这个结果转化为大模型可以阅读的物理格式,并以正确的时序重新塞回大模型的上下文(Messages 数组)中,唤醒大模型继续接龙。
|
|
10
|
+
|
|
11
|
+
这一过程在 Agent 架构中被称为 **Observation(观察事实)反向注入**。
|
|
12
|
+
|
|
13
|
+
看似简单的“把结果塞回去”,在底座协议层面却有着极为严苛的物理约束。一旦顺序错乱或漏掉了某条关键消息,大厂的 API 网关就会立刻拒绝服务。本节我们将以系统中真实存在的文件读取工具 `read_file` 为例,详细拆解这一归流过程的物理细节。
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 一、 为什么要进行反向注入?注意力时序的重建
|
|
18
|
+
|
|
19
|
+
在 1.1 节中我们确立了:大模型 API 本身是没有记忆的,多轮会话全靠底座回传历史剧本。
|
|
20
|
+
|
|
21
|
+
当大模型做出了 `Action`(输出工具调用意图)后,它的推理实际上已经在 API 服务器端戛然而止了。为了让它接着往下续写(比如向用户展示文件中的具体内容),底座必须重建整个**思考、行动与反馈**的时序线。
|
|
22
|
+
|
|
23
|
+
大模型在阅读回传的历史剧本时,需要建立起以下语义绑定:
|
|
24
|
+
* “我之前产生了一个想要读取文件的**意图**。”
|
|
25
|
+
* “底座执行了,并回传了这个意图的**结果**。”
|
|
26
|
+
* “我现在必须基于这个**结果**,继续撰写最终的回答。”
|
|
27
|
+
|
|
28
|
+
如果底座没有在 Messages 列表中完美还原这个“意图 -> 结果”的物理交替,大模型的自注意力层就会发生逻辑断裂,失去对当前任务进度的控制。
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 二、 OpenAI 规范下的物理消息回传时序
|
|
33
|
+
|
|
34
|
+
在标准的 Chat Completion 协议中,Observation 反向注入必须遵循一套严格的**四段式消息时序**。
|
|
35
|
+
|
|
36
|
+
让我们以读取工作区文件 `notes.txt` 为例,用时序图和真实的物理 Message 数组演变来拆解这一过程:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
客户端 (底座) LLM API 服务端
|
|
40
|
+
│ │
|
|
41
|
+
├─────── 1. 发送 User 提问: "读取 notes.txt" ───────>│
|
|
42
|
+
│ │ (推理)
|
|
43
|
+
|<────── 2. 返回 Tool Call 意图 (call_read_01) ─────┤
|
|
44
|
+
│ │ (暂停)
|
|
45
|
+
│ (底座在本地执行 read_file 工具) │
|
|
46
|
+
│ │
|
|
47
|
+
├─────── 3. 反向注入 Tool 执行结果 (Observation) ──>│
|
|
48
|
+
│ │ (继续推理)
|
|
49
|
+
|<────── 4. 返回最终文本: "文件内容为:..." ────────┤
|
|
50
|
+
▼ ▼
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 物理数据流的演变细节:
|
|
54
|
+
|
|
55
|
+
#### 步骤 1:底座向 API 发送初始提问
|
|
56
|
+
```json
|
|
57
|
+
[
|
|
58
|
+
{"role": "user", "content": "请读取工作区下的 notes.txt 文件内容"}
|
|
59
|
+
]
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
#### 步骤 2:API 暂停接龙,返回 Tool Call 申请
|
|
63
|
+
此时大模型吐回的 Response 包含 `tool_calls`。**注意:底座在本地获取到结果后,必须先将这条包含 `tool_calls` 意图的 `assistant` 消息完整地追加进会话历史中。**
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
[
|
|
67
|
+
{"role": "user", "content": "请读取工作区下的 notes.txt 文件内容"},
|
|
68
|
+
{
|
|
69
|
+
"role": "assistant",
|
|
70
|
+
"content": null,
|
|
71
|
+
"tool_calls": [
|
|
72
|
+
{
|
|
73
|
+
"id": "call_read_01",
|
|
74
|
+
"type": "function",
|
|
75
|
+
"function": { "name": "read_file", "arguments": "{\"path\":\"notes.txt\"}" }
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
#### 步骤 3:底座执行工具,执行 Observation 反向注入
|
|
83
|
+
底座在本地运行得到真实文件内容,并将其以 `role: "tool"` 的身份追加到刚才的 `assistant` 消息后面。**必须使用 `tool_call_id` 与刚才的 `call_read_01` 强行绑定!**
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
[
|
|
87
|
+
{"role": "user", "content": "请读取工作区下的 notes.txt 文件内容"},
|
|
88
|
+
{
|
|
89
|
+
"role": "assistant",
|
|
90
|
+
"content": null,
|
|
91
|
+
"tool_calls": [
|
|
92
|
+
{ "id": "call_read_01", "type": "function", "function": { "name": "read_file", "arguments": "{\"path\":\"notes.txt\"}" } }
|
|
93
|
+
]
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"role": "tool",
|
|
97
|
+
"tool_call_id": "call_read_01", // 💡 物理绑定防线:指示该结果属于 call_read_01
|
|
98
|
+
"content": "今日待办清单:\n1. 重构工具注册表\n2. 校验 Tool 协议时序"
|
|
99
|
+
}
|
|
100
|
+
]
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
#### 步骤 4:API 接收完整剧本,输出最终回答
|
|
104
|
+
大模型通过注意力机制扫描整个数组,自动将 `tool_call_id: "call_read_01"` 与上一行的 `assistant` 意图连接起来,得知 `read_file` 的结果是待办清单内容,顺利接龙输出最终文本:
|
|
105
|
+
|
|
106
|
+
```json
|
|
107
|
+
[
|
|
108
|
+
// ... 前面所有的历史 ...
|
|
109
|
+
{"role": "assistant", "content": "已为您读取 notes.txt 文件,内容包含两条今日待办:1. 重构工具注册表;2. 校验 Tool 协议时序。"}
|
|
110
|
+
]
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## 三、 协议完整性红线:为什么不能删掉 tool_calls 消息?
|
|
116
|
+
|
|
117
|
+
很多新手开发者在写底座时,觉得把 `assistant` 里的 `tool_calls` 发送回去太占 Token,或者觉得既然结果已经拿到了,直接把第 2 步的 `assistant` 消息删掉,只发送 `user` 提问和 `tool` 结果回去。
|
|
118
|
+
|
|
119
|
+
**这是最致命的协议错误。**
|
|
120
|
+
|
|
121
|
+
如果你删掉了中间的 `assistant` 消息,发给大模型的剧本会变成:
|
|
122
|
+
1. `user`: "请读取工作区下的 notes.txt 文件内容"
|
|
123
|
+
2. `tool` (call_read_01): "今日待办清单..."
|
|
124
|
+
|
|
125
|
+
大厂的 API 网关在接收到这个请求时,会立刻抛出协议异常:
|
|
126
|
+
`BadRequestError: 400 - 'messages' must contain a message with 'tool_calls' corresponding to 'tool_call_id' 'call_read_01' before the 'tool' message.`
|
|
127
|
+
|
|
128
|
+
* **物理原因**:因为大模型的自注意力层**必须建立连续的因果关系链**。没有意图(`tool_calls`)就直接抛出结果(`tool`),会导致注意力矩阵的因果依赖关系断裂。网关在协议层对此进行了死锁拦截。
|
|
129
|
+
|
|
130
|
+
本节我们深入解剖了 Observation 结果在多轮交互中的物理回溯和时序拼接要求。在下一节中,我们将对比智能体开发中最让人头疼的两大厂商阵营 —— OpenAI 与 Gemini 两大流派的 Function Calling 协议差异。
|
|
131
|
+
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "5.2 两大厂商协议流派交锋"
|
|
3
|
+
weight: 20
|
|
4
|
+
description: "对比 OpenAI 与 Gemini 在 Function Calling 上的底层数据包结构,解析 call_id 显式关联与函数名隐式配对的设计哲学差异。"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 5.2 两大厂商协议流派交锋
|
|
8
|
+
|
|
9
|
+
在 5.1 节中,我们了解了在 OpenAI 规范下,工具执行结果是如何通过 `tool_call_id` 进行 Observation 反向注入的。这似乎已经成为了行业的共识。
|
|
10
|
+
|
|
11
|
+
然而,在真实的多模型融合智能体(Multi-LLM Agent)开发中,我们会撞上一个令人头疼的物理现实:**大模型两大巨头 —— OpenAI 与 Google Gemini,在 Function Calling 的底层协议设计上,走向了完全不同的两条进化道路。**
|
|
12
|
+
|
|
13
|
+
如果不去抹平它们的协议差异,我们就根本无法实现“当 GPT-4o 限流时,底座全自动无缝降级到 Gemini 1.5 Pro 继续运行”这一高可用目标。
|
|
14
|
+
|
|
15
|
+
本节我们将对比解剖这两大厂商在多轮工具关联协议上的数据结构与设计哲学交锋。
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 一、 OpenAI 流派:基于唯一 call_id 的显式强关联
|
|
20
|
+
|
|
21
|
+
OpenAI 阵营(包括 Azure OpenAI、Anthropic Claude、Llama 3 等)的协议设计,是一种**“显式强关联”**的设计哲学。
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
[LLM 意图] ──带上随机 id: "call_abc123" ──> [底座执行] ──带回对应 id: "call_abc123" ──> [LLM 回溯]
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### 1. 数据包关键特征
|
|
28
|
+
大模型服务器在返回 `tool_calls` 时,会生成一个随机的 ID(如 `call_xyz888`)。底座在回传 Observation 消息时,**必须**将该 ID 写入 `tool_call_id` 属性。
|
|
29
|
+
* **物理设计意图**:通过一个不依赖文本上下文位置的**全局唯一 ID**,来死死锚定每一个工具意图与它的结果。
|
|
30
|
+
|
|
31
|
+
### 2. 优缺点物理评估
|
|
32
|
+
* **优点(应对高并发与并发调用)**:
|
|
33
|
+
由于每个调用都有专属的身份证,OpenAI 支持极强的**并行工具调用(Parallel Tool Calls)**。大模型可以一次性抛出 5 个工具调用,底座并发执行后,返回 5 条带有不同 `tool_call_id` 的消息。大模型在注意力层计算时,通过 ID 配对,能 100% 精确归流,不会产生乱序。
|
|
34
|
+
* **缺点(协议极度敏感)**:
|
|
35
|
+
协议过于繁琐和紧密。一旦底座在重试或截断历史时,把某个 `call_id` 拼错了一个字符,或者漏掉了一条意图消息,网关会判定逻辑因果链断裂,直接报 `400 BadRequest` 挂起。
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 二、 Gemini 流派:基于函数名的隐式关联
|
|
40
|
+
|
|
41
|
+
Google Gemini 阵营在设计 Function Calling 协议时,摒弃了繁琐的 `call_id`,选择了一种**“隐式轻量”**的设计哲学。
|
|
42
|
+
|
|
43
|
+
在 Gemini 官方的 `generateContent` 协议数据包中,它不使用 `tool_calls` 和 `tool` 角色,而是使用 `functionCalls` 和 `functionResponses`:
|
|
44
|
+
|
|
45
|
+
### 1. Gemini 协议输入数据包标本:
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"contents": [
|
|
49
|
+
{
|
|
50
|
+
"role": "user",
|
|
51
|
+
"parts": [{ "text": "同时读取工作区下的 notes.txt 和 todo.txt 文件" }]
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"role": "model",
|
|
55
|
+
"parts": [
|
|
56
|
+
{
|
|
57
|
+
"functionCalls": [
|
|
58
|
+
{
|
|
59
|
+
"name": "read_file",
|
|
60
|
+
"args": { "path": "notes.txt" }
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"name": "read_file",
|
|
64
|
+
"args": { "path": "todo.txt" }
|
|
65
|
+
}
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
]
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"role": "user",
|
|
72
|
+
"parts": [
|
|
73
|
+
{
|
|
74
|
+
"functionResponse": {
|
|
75
|
+
"name": "read_file", // 💡 没有 call_id,依靠函数名进行关联!
|
|
76
|
+
"response": { "result": "今日待办:1. 重构工具注册表" }
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"functionResponse": {
|
|
81
|
+
"name": "read_file",
|
|
82
|
+
"response": { "result": "开发计划:下周上线多模型代理" }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
]
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### 2. 物理设计意图
|
|
92
|
+
Gemini 协议中没有任何随机生成的 ID 概念!它是**直接通过函数名 `name` 进行隐式对齐**的。
|
|
93
|
+
* 在大模型推理时,如果大模型返回了 `name: "read_file"` 的调用,底座反向注入的 `functionResponse` 也必须携带 `name: "read_file"`。
|
|
94
|
+
* 在大模型内部,多轮会话关联通常通过注意力矩阵对函数名 `name` 以及输入参数 `args` 与返回结果 `response` 的位置邻近性(Locality)来进行隐式对齐。
|
|
95
|
+
|
|
96
|
+
### 3. 优缺点物理评估
|
|
97
|
+
* **优点(简单轻量)**:
|
|
98
|
+
底座软件不需要生成并记录一堆临时的 `call_id`。如果我们需要在本地 mock 工具调用或者在测试环境伪造数据,极其简单,不需要防范 ID 错位。
|
|
99
|
+
* **缺点(并行调用易错乱)**:
|
|
100
|
+
当发生并发调用时(如上面同时读取两个文件),返回的两条 `functionResponse` 的 `name` **全是 `read_file`**。
|
|
101
|
+
如果这两个文件读取接口的返回顺序发生颠倒,或者模型对参数的提取存在歧义,注意力层在隐式配对时,极易将 `todo.txt` 的内容错配给 `notes.txt`,或者因为重名发生混淆(Lost in Mapping),这是隐式关联天然的物理硬伤。
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## 三、 【调试与避坑】抹平两者的网络时序陷阱
|
|
106
|
+
|
|
107
|
+
作为一个支持多提供商的 Agent 系统架构师,如果你想让你的 `agent-executor.ts` 保持纯净,就绝对不能在执行器里写出“如果是 OpenAI 怎么拼,如果是 Gemini 怎么拼”的代码。这违反了微内核的解耦原则。
|
|
108
|
+
|
|
109
|
+
### 避坑策略:统一底座 LLMMessage 协议标准
|
|
110
|
+
在 Freya 中,我们采取的方案是:**底座只承认并记录最通用、表达力最强的 OpenAI `role: "tool"` 格式作为统一的 `LLMMessage` 数据格式。**
|
|
111
|
+
|
|
112
|
+
当消息流向具体的模型驱动插件时,由插件自身在最后一微秒进行**物理双向翻译(Bidirectional Translation)**:
|
|
113
|
+
|
|
114
|
+
#### 翻译路径 A:OpenAI 插件
|
|
115
|
+
由于底座已经是 OpenAI 格式,OpenAI 驱动插件几乎是“零转换”直接透传给官方 SDK。
|
|
116
|
+
|
|
117
|
+
#### 翻译路径 B:Gemini 插件
|
|
118
|
+
当底座将 `LLMMessage[]` 投递给 `@eoasmxd/freya-plugin-gemini` 插件时:
|
|
119
|
+
1. **意图翻译**:插件扫描消息,将 `{ role: "assistant", toolCalls: [...] }` 翻译成 Gemini 的 `{ role: "model", parts: [{ functionCalls: [...] }] }`。
|
|
120
|
+
2. **ID 剥离与名称对齐**:将 `{ role: "tool", toolCallId: "call_123", content: "..." }` 中的 ID 丢弃,提取其关联的工具函数名,组装成 Gemini 格式的 `functionResponse: { name: "read_file", response: { result: "..." } }`。
|
|
121
|
+
3. **结果发回**:大模型预测后吐出新的 `functionCall` 意图,插件再在后台**自制一个随机 ID** 拼回底座标准的 `toolCalls` 对象中返回给执行器。
|
|
122
|
+
|
|
123
|
+
通过这一层插头式(Adapter)的翻译设计,Freya 在完全不改动 ReAct 核心决策控制流的前提下,完美地屏蔽了太平洋两岸两大巨头的协议流派差异,实现了底座级的自动高可用降级。
|
|
124
|
+
|
|
125
|
+
在下一节中,我们将实际进入 Freya 源码,去白盒解剖执行这一双向翻译并抹平差异的代理层设计。
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "5.3 【白盒剖析】Freya 大模型代理层协议映射"
|
|
3
|
+
weight: 30
|
|
4
|
+
description: "白盒解剖 Freya 的 plugin-gemini 核心源码,探秘如何物理回溯历史找回工具名并内存伪造 call_id 抹平协议差异。"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 5.3 【白盒剖析】Freya 大模型代理层协议映射
|
|
8
|
+
|
|
9
|
+
在 5.2 节中,我们对比了 OpenAI 显式 `call_id` 强关联与 Gemini 隐式函数名对齐的设计哲学差异。在工程上,为了保持内核 `agent-executor.ts` 的极致纯净,底座核心不需要也不应该知道这些差异。所有的适配和抹平工作,全部下沉到了各个模型的**驱动插件(Plugin)**中。
|
|
10
|
+
|
|
11
|
+
本节我们将实际白盒解剖 Freya 核心插件(位于 `plugins/plugin-gemini/src/index.ts`)的源码实现,看看插件是如何在最后一微秒执行**双向协议翻译(Bidirectional Translation)**,在没有修改内核一行代码的前提下,完美抹平两大巨头协议鸿沟的。
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 一、 架构脉络:LLM 统一代理与插件契约
|
|
16
|
+
|
|
17
|
+
当内核执行器需要发起对话时,它调用统一的 `llm-proxy.ts` 代理服务。代理服务会找到注册的 `GeminiPlugin` 实例,并调用其 `chat()` 接口。
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
export default class GeminiPlugin implements LLMPlugin {
|
|
21
|
+
type = 'llm' as const;
|
|
22
|
+
providerTypes = ['gemini'];
|
|
23
|
+
private context!: FreyaContext;
|
|
24
|
+
|
|
25
|
+
async chat(
|
|
26
|
+
messages: LLMMessage[],
|
|
27
|
+
tools?: ToolDefinition[],
|
|
28
|
+
options?: LLMPluginOptions
|
|
29
|
+
): Promise<{ message: LLMMessage; usage?: LLMTokenUsage }> {
|
|
30
|
+
// 所有的协议抹平工作都在此方法内部异步完成
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 二、 翻译第一向:底座通用消息 -> Gemini 原生请求体
|
|
38
|
+
|
|
39
|
+
在向 Gemini 官方 API 发起 HTTP 请求前,插件必须先将底座中标准的 `LLMMessage[]` 数组,翻译为 Gemini 识别的 `contents` 树:
|
|
40
|
+
|
|
41
|
+
### 1. 助手消息意图转换
|
|
42
|
+
当扫描到 `role: "assistant"` 且带有 `toolCalls` 时,插件需要将其转化为 Gemini 的 `functionCall`:
|
|
43
|
+
```typescript
|
|
44
|
+
if (msg.role === 'assistant') {
|
|
45
|
+
const parts: any[] = [];
|
|
46
|
+
if (msg.content) {
|
|
47
|
+
parts.push({ text: msg.content });
|
|
48
|
+
}
|
|
49
|
+
if (msg.toolCalls && msg.toolCalls.length > 0) {
|
|
50
|
+
const sig = msg.thoughtSignature; // 捕获 Gemini 的思维上下文指纹
|
|
51
|
+
for (const tc of msg.toolCalls) {
|
|
52
|
+
parts.push({
|
|
53
|
+
functionCall: {
|
|
54
|
+
name: tc.name,
|
|
55
|
+
args: JSON.parse(tc.arguments || '{}') // 反序列化为 args 对象
|
|
56
|
+
},
|
|
57
|
+
...(sig ? { thoughtSignature: sig } : {}) // 如果存在 thoughtSignature,强行透传回去
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return { role: 'model', parts }; // 映射为 model 角色
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### 2. 工具结果的“函数名找回”算法 (无 call_id 的救赎)
|
|
66
|
+
这是整个翻译中最精彩的物理逻辑。因为底座的 `role: "tool"` 消息只记录了 `toolCallId`,而没有直接存放工具名称;但在 Gemini 协议中,反向注入必须携带 `name`。
|
|
67
|
+
|
|
68
|
+
插件使用了一段**物理倒读历史消息**的算法,瞬时找回了丢失的工具名:
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
if (msg.role === 'tool') {
|
|
72
|
+
let toolName = msg.toolName;
|
|
73
|
+
|
|
74
|
+
// 如果底座消息中缺乏 toolName 但带有 toolCallId
|
|
75
|
+
if (!toolName && msg.toolCallId) {
|
|
76
|
+
const idx = messages.indexOf(msg);
|
|
77
|
+
if (idx > 0) {
|
|
78
|
+
// 💡 物理回溯:从当前 tool 消息位置倒着往回扫描历史记录
|
|
79
|
+
for (let i = idx - 1; i >= 0; i--) {
|
|
80
|
+
const prev = messages[i];
|
|
81
|
+
// 找到最近的那个带有 toolCalls 的助手意图消息
|
|
82
|
+
if (prev.role === 'assistant' && prev.toolCalls) {
|
|
83
|
+
// 比对 call_id 匹配,找回其对应的工具函数注册名
|
|
84
|
+
const match = prev.toolCalls.find((tc) => tc.id === msg.toolCallId);
|
|
85
|
+
if (match) {
|
|
86
|
+
toolName = match.name; // 成功找回!
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 翻译为 Gemini 规范的 functionResponse 结构
|
|
95
|
+
return {
|
|
96
|
+
role: 'user', // Gemini 协议要求工具结果角色为 user
|
|
97
|
+
parts: [
|
|
98
|
+
{
|
|
99
|
+
functionResponse: {
|
|
100
|
+
name: toolName || 'default_tool',
|
|
101
|
+
response: { result: msg.content }
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
]
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
通过这段回溯算法,插件在最后一微秒把 OpenAI 的 `call_id` 翻译为了 Gemini 必须的 `functionResponse.name` 显式对齐,重建了上下文因果链。
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## 三、 翻译第二向:Gemini 返回 -> 底座通用消息 (ID 伪造)
|
|
114
|
+
|
|
115
|
+
当 Gemini 官方 API 推送回响应,决定发起工具调用时,它吐出的是 `{ functionCall: { name: "read_file", args: { path: "notes.txt" } } }`。
|
|
116
|
+
|
|
117
|
+
由于内核执行器 `agent-executor.ts` 在并发调度多个 Promise 时必须依靠 `toolCall.id` 进行事件映射。为了不让执行器抛空报错,插件在接收到响应时,**在内存中主动伪造了一个随机 ID**:
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
const candidate = json.candidates?.[0];
|
|
121
|
+
const parts = candidate?.content?.parts || [];
|
|
122
|
+
|
|
123
|
+
let textContent = '';
|
|
124
|
+
const toolCalls: any[] = [];
|
|
125
|
+
let thoughtSignature: string | undefined;
|
|
126
|
+
|
|
127
|
+
for (const part of parts) {
|
|
128
|
+
if (part.text) {
|
|
129
|
+
textContent += part.text;
|
|
130
|
+
}
|
|
131
|
+
// 1. 如果大模型输出了工具调用
|
|
132
|
+
if (part.functionCall) {
|
|
133
|
+
toolCalls.push({
|
|
134
|
+
// 💡 内存伪造:使用加密随机数算法自动生成一个符合 OpenAI 风格的 call_id
|
|
135
|
+
id: `call_${Math.random().toString(36).substring(2, 11)}`,
|
|
136
|
+
name: part.functionCall.name,
|
|
137
|
+
arguments: JSON.stringify(part.functionCall.args || {}) // 序列化为 arguments 字符串
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
// 2. 捕获 Gemini 的思维签名指纹,暂存内存,用于下一轮透传
|
|
141
|
+
const sig = part.thoughtSignature;
|
|
142
|
+
if (sig) {
|
|
143
|
+
thoughtSignature = sig;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const message: LLMMessage = {
|
|
148
|
+
role: 'assistant',
|
|
149
|
+
content: textContent,
|
|
150
|
+
toolCalls: toolCalls.length > 0 ? toolCalls : undefined
|
|
151
|
+
};
|
|
152
|
+
if (thoughtSignature) {
|
|
153
|
+
message.thoughtSignature = thoughtSignature;
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### 物理抹平的艺术:
|
|
158
|
+
在这层逻辑下,Gemini 原生不带 ID 的 `functionCall` 被天衣无缝地包装成了带 `call_xxxx` 的标准 `toolCalls` 对象。内核执行器拿着这个伪造的 ID 进行了完美的并发 Tool 调度,并在 Observation 返回时又带回了该 ID,而插件又在下一轮发送时通过倒读把该 ID 抹去、恢复为函数名发给 Gemini API。
|
|
159
|
+
|
|
160
|
+
整个“伪造与回滚”的过程,完全被隔离在插件内部。这就是 Freya 底座优雅的协议抹平艺术。
|
|
161
|
+
|
|
162
|
+
本节我们白盒看清了插件双向解包、倒读历史以及内存伪造 ID 抹平协议差异的TypeScript实现。在下一节中,我们将在本地亲自调试,去解决由于“多工具并发调用时 ID 乱序”带来的网络风暴。
|