April 22, 2026 • 版本: v2026.3.1

[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.1Body可被 formatInboundEnvelope 修改
≥ v2026.3.1BodyForAgent无法被 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添加 fromMesenderName 字段检查 inbound.fromMe 是 boolean
process-message.jsbuildInboundLine 传递 fromMe检查第三个参数存在
build-inbound-line.js解构并转发 fromMesenderName检查参数正确传递
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]: 前缀格式特意设计为简洁,以最小化令牌开销,同时提供必要的归属。


相关错误:BodyForAgentsenderName 未定义

错误模式:

TypeError: Cannot read property 'senderName' of undefined
    at finalizeInboundContext (finalize-inbound-context.js:42)
    at processMessage (process-message.js:87)

原因: senderNameinboundMessage 传播完成之前被访问。

解决: 确保在调用 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 作为单独的字段,绕过了信封转换。

影响: 此架构变更造成了此修复解决的传播缺口。

依据与来源

本故障排除指南由 FixClaw 智能管线从社区讨论中自动合成。