[LLM 上下文中私信消息缺少发送者归属] - DM Messages Lack Sender Attribution in LLM Context (BodyForAgent)
私信对话对 LLM 而言以无差别的文本流形式呈现,因为发送者标识未通过入站管道传递,且 BodyForAgent 缺少发送者前缀。
🔍 症状
主要表现
当 Agent 处理私信对话时,LLM 收到的消息不包含发送者上下文。请看这个私信对话:
LLM 实际收到的内容(当前行为):
Hey, are you free tonight?
Yes, I'll be there at 8
Great, see you then!
Looking forward to it!LLM 应该收到的内容(预期行为):
[Alice]: Hey, are you free tonight?
[Agent]: Yes, I'll be there at 8
[Alice]: Great, see you then!
[Agent]: Looking forward to it!技术观察
fromMe 标志存在于协议适配器层面,但无法被 LLM 获取:
// 适配器层面(以 WhatsApp 为例):
msg.key.fromMe // Boolean - 正确识别发送者
// LLM 输入层面(BodyForAgent):
params.msg.body // "Hey, are you free tonight?" — 无发送者上下文诊断命令
检查 inboundMessage 的当前状态:
# 启用调试日志以观察入站消息结构
DEBUG=openclaw:inbound node agent.js
# 预期调试输出,显示缺失的字段:
# inboundMessage {
# body: "Hey, are you free tonight?",
# from: "+1234567890",
# pushName: "Alice",
# chatType: "direct",
# // fromMe: undefined ← 缺失
# // senderName: undefined ← 未传播
# }版本特定行为
此问题自 v2026.3.1 起出现,因为框架改为向 LLM 传递 BodyForAgent 而非 Body。对 formatInboundEnvelope 的更改只影响 Body,对 LLM 不可见。
🧠 根因分析
架构缺陷:缺失的传播路径
根本原因是协议适配器和 LLM 上下文之间的数据流断裂。fromMe 标志和 senderName 在管道早期可用,但未通过整个链路传播到 BodyForAgent。
数据流分析
┌─────────────────────────────────────────────────────────────────────────┐ │ CURRENT DATA FLOW │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ Protocol Adapter │ │ ├── msg.key.fromMe = true/false ✓ AVAILABLE │ │ ├── msg.pushName = “Alice” ✓ AVAILABLE │ │ └── msg.chatType = “direct” ✓ AVAILABLE │ │ │ │ │ ▼ │ │ inboundMessage CONSTRUCTOR │ │ └── fromMe field NOT ADDED ✗ DROPPED HERE │ │ │ │ │ ▼ │ │ processMessage → buildInboundLine → formatInboundEnvelope │ │ └── fromMe still undefined ✗ NOT FORWARDED │ │ │ │ │ ▼ │ │ finalizeInboundContext caller │ │ └── BodyForAgent lacks [senderName]: prefix ✗ LLM RECEIVES AMBIGUOUS DATA │ │ └─────────────────────────────────────────────────────────────────────────┘
代码级分析
1. inboundMessage 构造(丢弃点 #1)
javascript // Current implementation - missing fromMe field function inboundMessage(msg, chatId) { return { body: msg.body || msg.text || “”, from: msg.from || msg.chat?.id, pushName: msg.pushName || msg.sender?.first_name, chatType: msg.chatType || (msg.chat?.isGroup ? “group” : “direct”), timestamp: msg.timestamp || Date.now(), // MISSING: fromMe: Boolean(msg.key?.fromMe) // MISSING: senderName: extractSenderName(msg) }; }
2. formatInboundEnvelope(不影响 LLM)
javascript
// This function modifies Body, which is NOT what the LLM receives
function formatInboundEnvelope(params) {
const selfMarker = params.fromMe ? “[You]: " : “”;
return {
Body: ${selfMarker}${params.body},
// BodyForAgent is NOT set here — LLM receives the raw value
};
}
3. finalizeInboundContext 调用方(丢弃点 #2)
构建 BodyForAgent 的最终转换不包含发送者前缀逻辑:
javascript // Current implementation const BodyForAgent = params.msg.body; // Raw text, no attribution
为什么群组消息正常工作
群组消息本身就包含发送者归属,因为消息格式已包含发送者名称:
javascript // Group messages already have this structure from the protocol: “[GroupName] @Alice: message content” // or “[Alice]: message content”
私信消息缺少这种结构前缀,因此如果没有显式处理,发送者识别将变得不可能。
版本回归分析
| 版本 | LLM 输入 | 行为 |
|---|---|---|
| < v2026.3.1 | Body | 可被 formatInboundEnvelope 修改 |
| ≥ v2026.3.1 | BodyForAgent | 无法被 formatInboundEnvelope 修改 |
v2026.3.1 的重构引入了直接的 BodyForAgent 字段,绕过了 formatInboundEnvelope 转换,造成了此缺陷。
🛠️ 逐步修复
此修复需要对入站管道中的五个函数进行修改。请按列出的顺序应用更改以保持数据完整性。
阶段 1:通过管道传播 fromMe
步骤 1.1:在 inboundMessage 构造中添加 fromMe
文件: src/core/message/inbound-message.js
修改前: javascript function inboundMessage(msg, chatId) { return { body: msg.body || msg.text || “”, from: msg.from || msg.chat?.id, pushName: msg.pushName || msg.sender?.first_name, chatType: msg.chatType || (msg.chat?.isGroup ? “group” : “direct”), timestamp: msg.timestamp || Date.now(), // MISSING }; }
修改后: javascript function inboundMessage(msg, chatId) { return { body: msg.body || msg.text || “”, from: msg.from || msg.chat?.id, pushName: msg.pushName || msg.sender?.first_name, chatType: msg.chatType || (msg.chat?.isGroup ? “group” : “direct”), timestamp: msg.timestamp || Date.now(), fromMe: Boolean(msg.key?.fromMe), // ADD: Propagate fromMe flag senderName: msg.pushName || msg.sender?.first_name || msg.sender?.username || “Unknown”, // ADD: Sender name extraction }; }
步骤 1.2:通过 processMessage 传递 fromMe
文件: src/core/message/process-message.js
修改前: javascript async function processMessage(msg, chatId, context) { const inbound = inboundMessage(msg, chatId);
// … other processing …
await buildInboundLine(inbound, context); }
修改后: javascript async function processMessage(msg, chatId, context) { const inbound = inboundMessage(msg, chatId);
// … other processing …
await buildInboundLine(inbound, context, { fromMe: inbound.fromMe }); }
步骤 1.3:在 buildInboundLine 中解构并转发 fromMe
文件: src/core/message/build-inbound-line.js
修改前: javascript async function buildInboundLine(inbound, context) { const { body, from, pushName, chatType, timestamp } = inbound;
// … processing …
await formatInboundEnvelope({ msg: { body, from, pushName, chatType, timestamp }, conversation: context.conversation, }); }
修改后: javascript async function buildInboundLine(inbound, context, options = {}) { const { body, from, pushName, chatType, timestamp, fromMe, senderName } = inbound;
// … processing …
await formatInboundEnvelope({ msg: { body, from, pushName, chatType, timestamp, fromMe, senderName }, conversation: context.conversation, fromMe: options.fromMe ?? fromMe, }); }
步骤 1.4:在 formatInboundEnvelope 中添加自标记(适用于 Body)
文件: src/core/message/format-inbound-envelope.js
修改前: javascript function formatInboundEnvelope(params) { return { Body: params.msg.body, // … other fields }; }
修改后: javascript function formatInboundEnvelope(params) { const selfMarker = params.fromMe ? “[You]: " : “”;
return {
Body: ${selfMarker}${params.msg.body},
// … other fields
};
}
阶段 2:为 BodyForAgent 添加发送者前缀
步骤 2.1:修改 finalizeInboundContext 调用方
文件: src/core/context/finalize-inbound-context.js
修改前: javascript function finalizeInboundContext(params) { // … other processing …
const BodyForAgent = params.msg.body;
return { // … other fields BodyForAgent, }; }
修改后: javascript function finalizeInboundContext(params) { // … other processing …
// Add sender prefix for DMs only; group messages already have attribution
const dmPrefix = params.msg.chatType !== “group”
? [${params.msg.senderName || params.msg.from || "Unknown"}]: : “”;
const BodyForAgent = ${dmPrefix}${params.msg.body};
return { // … other fields BodyForAgent, }; }
阶段 3:验证清单
应用所有更改后,请验证以下修改:
| 文件 | 更改 | 验证方法 |
|---|---|---|
inbound-message.js | 添加 fromMe 和 senderName 字段 | 检查 inbound.fromMe 是 boolean |
process-message.js | 向 buildInboundLine 传递 fromMe | 检查第三个参数存在 |
build-inbound-line.js | 解构并转发 fromMe、senderName | 检查参数正确传递 |
format-inbound-envelope.js | 向 Body 添加 [You]: 自标记 | 在日志中检查 Body 格式 |
finalize-inbound-context.js | 为私信添加 [Name]: 前缀 | 检查 BodyForAgent 格式 |
🧪 验证
验证方法 1:单元测试验证
创建并运行以下测试以验证传播链路:
// test/inbound-propagation.test.js
const { inboundMessage } = require('../src/core/message/inbound-message');
const { processMessage } = require('../src/core/message/process-message');
const { buildInboundLine } = require('../src/core/message/build-inbound-line');
const { formatInboundEnvelope } = require('../src/core/message/format-inbound-envelope');
const { finalizeInboundContext } = require('../src/core/context/finalize-inbound-context');
describe('fromMe propagation and sender identification', () => {
const mockMsg = {
body: "Test message",
from: "+1234567890",
pushName: "Alice",
chatType: "direct",
timestamp: Date.now(),
key: { fromMe: false },
};
const mockMsgFromMe = {
...mockMsg,
key: { fromMe: true },
};
test('inboundMessage includes fromMe and senderName', () => {
const result = inboundMessage(mockMsg, 'chat123');
expect(result.fromMe).toBe(false);
expect(result.senderName).toBe('Alice');
});
test('inboundMessage.fromMe is true when key.fromMe is true', () => {
const result = inboundMessage(mockMsgFromMe, 'chat123');
expect(result.fromMe).toBe(true);
});
test('BodyForAgent includes [senderName]: prefix for DMs', () => {
const context = { conversation: [] };
const result = finalizeInboundContext({
msg: { body: "Test", chatType: "direct", senderName: "Alice", from: "+1234567890" },
conversation: context.conversation,
});
expect(result.BodyForAgent).toBe('[Alice]: Test');
});
test('BodyForAgent has no prefix for group messages', () => {
const context = { conversation: [] };
const result = finalizeInboundContext({
msg: { body: "Test", chatType: "group", senderName: "Alice", from: "+1234567890" },
conversation: context.conversation,
});
expect(result.BodyForAgent).toBe('Test');
});
test('Body includes [You]: prefix when fromMe is true', () => {
const result = formatInboundEnvelope({
msg: { body: "My message", fromMe: true },
});
expect(result.Body).toBe('[You]: My message');
});
});
运行测试:
npm test -- test/inbound-propagation.test.js预期输出:
✓ inboundMessage includes fromMe and senderName
✓ inboundMessage.fromMe is true when key.fromMe is true
✓ BodyForAgent includes [senderName]: prefix for DMs
✓ BodyForAgent has no prefix for group messages
✓ Body includes [You]: prefix when fromMe is true验证方法 2:使用模拟协议适配器进行集成测试
javascript // test/dm-sender-attribution.test.js const { runFullPipeline } = require(’../src/core/test-helpers’);
async function testDMSenderAttribution() { const mockAdapter = { name: ‘mock’, sendMessage: jest.fn(), onMessage: (handler) => { // Simulate incoming DM from Alice handler({ key: { fromMe: false }, body: “Hey, are you free tonight?”, from: “+1111111111”, pushName: “Alice”, chatType: “direct”, timestamp: Date.now(), });
// Simulate outbound DM (fromMe = true)
handler({
key: { fromMe: true },
body: "Yes, I'll be there at 8",
from: "+2222222222",
pushName: "Agent",
chatType: "direct",
timestamp: Date.now(),
});
},
};
const agent = createAgent({ adapter: mockAdapter }); await agent.start();
// Capture the LLM input const llmInput = captureLLMInput();
console.log(‘LLM received BodyForAgent:’); console.log(llmInput.BodyForAgent);
// Expected output: // [Alice]: Hey, are you free tonight? // [Agent]: Yes, I’ll be there at 8
await agent.stop(); }
testDMSenderAttribution();
验证方法 3:手动调试日志
启用详细日志以检查整个管道:
# 设置环境变量
export DEBUG=openclaw:inbound,openclaw:context
export LOG_LEVEL=debug
# 运行 agent
node agent.js 2>&1 | grep -E "(fromMe|senderName|BodyForAgent|\[.*\]:)"
# 私信消息的预期日志输出:
# [debug] inboundMessage.fromMe: false
# [debug] inboundMessage.senderName: "Alice"
# [debug] BodyForAgent: "[Alice]: Hey, are you free tonight?"验证方法 4:数据库状态检查
如果使用持久化上下文,请验证存储的消息:
# 查询 messages 表
SELECT id, sender_name, from_me, body, body_for_agent
FROM messages
WHERE chat_type = 'direct'
ORDER BY timestamp DESC LIMIT 5;
-- 预期结果:
-- | id | sender_name | from_me | body | body_for_agent |
-- | 1 | Alice | false | Hey, free? | [Alice]: Hey, free? |
-- | 2 | Agent | true | Yes, at 8 | [Agent]: Yes, at 8 |⚠️ 常见陷阱
陷阱 1:协议适配器字段名称差异
问题: 不同的消息平台以不同的字段名暴露 fromMe。
- WhatsApp:
msg.key.fromMe - Telegram:
msg.from.is_bot(反向逻辑)或msg.outgoing - Signal:
msg.direction === "outgoing" - Discord:
msg.author.id === msg.client.user.id
缓解措施: 在 inboundMessage 构造器中创建适配器特定的字段映射器:
javascript function extractFromMe(msg, platform) { switch (platform) { case ‘whatsapp’: return Boolean(msg.key?.fromMe); case ’telegram’: return Boolean(msg.outgoing); case ‘signal’: return msg.direction === ‘outgoing’; case ‘discord’: return msg.author?.id === msg.client?.user?.id; default: return false; } }
陷阱 2:缺少 senderName 回退链
问题: pushName 可能不可用(用户启用了隐私设置,或在推送名称缓存之前的首条消息)。
缓解措施: 实现健壮的回退链:
javascript function extractSenderName(msg) { return ( msg.pushName || msg.sender?.first_name || msg.sender?.username || msg.from?.split(’@’)[0] || // 使用 JID/电话号码作为最后手段 “Unknown” ); }
陷阱 3:群组消息中的重复前缀
问题: 如果群组消息在协议负载中已包含发送者归属,添加另一个前缀会导致重复。
错误输出示例:
[#general] @Alice: [Alice]: Message content // 重复归属
缓解措施: 添加前缀前检查现有消息格式:
javascript function shouldAddPrefix(msg, chatType) { if (chatType !== ‘direct’) { // 检查群组消息是否已有归属模式 const existingPattern = /^[[^]]+]\s*@\w+:/; return !existingPattern.test(msg.body); } return true; }
陷阱 4:特定平台的发送者名称格式
问题: 不同平台格式化发送者名称的方式不同。
- WhatsApp: 显示名称(如 "John Smith")
- Telegram: 名 + 可选的姓
- Discord: 用户名 + 标识符(如 "User#1234")
缓解措施: 在使用前缀中的发送者名称之前先进行规范化:
javascript function normalizeSenderName(name, platform) { if (!name) return “Unknown”;
let normalized = name.trim();
if (platform === ‘discord’) { // 如果存在则移除标识符 normalized = normalized.split(’#’)[0]; }
// 移除可能破坏解析的特殊字符 return normalized.replace(/[[]]/g, ‘’).substring(0, 50); }
陷阱 5:v2026.3.1 之后的版本兼容性
问题: 如果代码库有基于使用 BodyForAgent 还是 Body 的条件逻辑,修复可能无法统一应用。
缓解措施: 验证 LLM 适配器实际使用哪个字段:
javascript // Check your LLM adapter configuration const llmAdapter = config.llm?.adapter;
// If using Body directly (old behavior), the formatInboundEnvelope fix applies // If using BodyForAgent (new behavior), the finalizeInboundContext fix applies // Some adapters may use both — ensure consistency
陷阱 6:并行消息处理中的竞态条件
问题: 同时处理多条私信时,fromMe 状态可能过时或与不同的消息上下文错误关联。
缓解措施: 确保 fromMe 在构造时绑定到特定消息上下文,而不是全局检索:
javascript // WRONG: Global state reference inboundMessage.fromMe = globalLastMessageFromMe; // Race condition
// CORRECT: Message-specific extraction inboundMessage.fromMe = Boolean(msg.key?.fromMe); // Isolated per message
🔗 相关错误
相关问题 #32060:在上下文中包含出站私信
症状: Agent 只接收收到的私信,但不接收 Agent 自己发送的出站消息,使对话看起来是单方面的。
关联: 此问题是当前修复的补充。#32060 确保出站消息存储在上下文中,而此修复确保入站和出站消息都有正确的发送者归属。
解决依赖: 此修复中实现的 fromMe 传播是正确实现 #32060 所必需的。
相关问题 #32059:私信上下文窗口溢出
症状: 长时间的私信对话消耗过多的上下文窗口令牌,因为每条消息都缺少高效的发送者标识。
关联: 此修复引入的 [Name]: 前缀格式特意设计为简洁,以最小化令牌开销,同时提供必要的归属。
相关错误:BodyForAgent 中 senderName 未定义
错误模式:
TypeError: Cannot read property 'senderName' of undefined
at finalizeInboundContext (finalize-inbound-context.js:42)
at processMessage (process-message.js:87)原因: senderName 在 inboundMessage 传播完成之前被访问。
解决: 确保在调用 finalizeInboundContext 之前 inboundMessage 构造包含 senderName 字段。
相关警告:fromMe 不是布尔值
警告模式:
Warning: BodyForAgent received non-boolean fromMe value: "true"
Warning: Self-marker logic may behave unexpectedly原因: fromMe 被存储为字符串(“true”/“false”)而不是布尔值。
解决: 在 inboundMessage 构造器中确保使用 Boolean(msg.key?.fromMe) 强制转换。
相关配置:dm.senderPrefix.enabled
配置键: openclaws.dm.senderPrefix.enabled
用途: 切换私信中的发送者前缀,用于测试或用户偏好。
默认值: true
交互: 禁用后,私信恢复为模糊格式;fromMe 传播仍可用于其他目的(分析、过滤)。
相关日志类别:openclaw:inbound:fromme
调试标志: DEBUG=openclaw:inbound:fromme
输出: 在每个管道阶段记录 fromMe 值,用于追踪传播失败。
历史记录:v2026.3.1 之前的行为
背景: 在 v2026.3.1 之前,框架使用 Body(由 formatInboundEnvelope 修改)作为 LLM 输入。在那里应用自标记修复会对 LLM 可见。
变更: v2026.3.1 引入了 BodyForAgent 作为单独的字段,绕过了信封转换。
影响: 此架构变更造成了此修复解决的传播缺口。