跳到主要内容

多用户 Thread

多个用户可以共享同一个 Mastra Thread,每个用户都有自己的姓名和职能角色。你可以在消息正文中携带发言者身份,让 Agent 在读取同一个共享 Thread 时区分不同用户。

何时使用多用户 Thread
何时使用多用户 Thread的直接链接

当多人通过同一个 Agent 围绕同一主题协作时,请使用多用户 Thread:

  • 包含编辑者、审阅者和批准者的协作文档
  • 由一个助手服务多位参与者的群聊
  • 不同角色拥有不同权限的多利益相关方审阅

让所有参与者共享一个 resourceId
share-one-resourceid-across-all-participants的直接链接

一个 Thread 只属于一个 resourceId,因此共享 Thread 的所有参与者都需要传入相同的值。不要使用用户 ID(单用户应用的默认做法),而应根据对话本身确定 resourceId,例如共享文档使用 doc_${docId},群聊使用 room_${roomId}。所有人都指向同一个 resourceId 后,就会读写同一份历史。

为每条用户消息标记发言者身份
为每条用户消息标记发言者身份的直接链接

模型需要知道每一轮是谁在发言。消息正文是唯一会保留在历史中并重新进入上下文的位置,因此请用一个简短的 <turn> 标签包裹每条用户消息,并在其中包含发言者的 ID、姓名和角色。该标签会始终附着在消息上,因此召回先前轮次时,模型仍能看到每句话是谁说的。

使用一个简短的辅助函数构建该标签。下面的示例展示了一种实现方式;请将其复制到你的项目中,并根据用户数据结构进行调整:

src/mastra/identity.ts
export type Speaker = {
id: string
name: string
role: string
}

function escapeAttr(value: string) {
return value
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
}

export function asUserTurn(speaker: Speaker, text: string) {
const id = escapeAttr(speaker.id)
const name = escapeAttr(speaker.name)
const role = escapeAttr(speaker.role)
return {
role: 'user' as const,
content: `<turn author_id="${id}" author_name="${name}" functional_role="${role}">
${text}
</turn>`,
}
}

在 Agent 的指令中教它如何读取 <turn> 标签。Agent 必须配置 memory,才能在调用时传入 threadresource

src/mastra/agents/collab.ts
import { Agent } from '@mastra/core/agent'
import { Memory } from '@mastra/memory'
import { LibSQLStore } from '@mastra/libsql'

const memory = new Memory({
storage: new LibSQLStore({ id: 'collab-storage', url: 'file:./collab.db' }),
options: {
lastMessages: 20,
},
})

export const collabAgent = new Agent({
id: 'collab',
name: 'CollabAgent',
model: 'openai/gpt-5-mini',
memory,
instructions: `
You are a collaborative document assistant. Multiple users talk to you in the SAME thread.

Every user message is wrapped in a <turn> tag carrying the user's identity:

<turn author_id="u_alice" author_name="Alice" functional_role="editor">
...message text...
</turn>

Rules:
1. Address users by their author_name.
2. Respect functional_role: editors propose changes, reviewers approve.
3. When attributing past statements, read author_name from the surrounding <turn> tag.
4. Do not echo the <turn> tags back at users.
`.trim(),
})

使用包裹后的消息调用 Agent。每位参与者都共享相同的 threadresource

src/mastra/call.ts
import { asUserTurn } from './identity'

const docResourceId = 'doc_42'
const docThreadId = 'doc_42'

const alice = { id: 'u_alice', name: 'Alice', role: 'editor' }
const bob = { id: 'u_bob', name: 'Bob', role: 'reviewer' }

await collabAgent.generate([asUserTurn(alice, 'My favorite color is teal.')], {
memory: { thread: docThreadId, resource: docResourceId },
})

await collabAgent.generate([asUserTurn(bob, 'I want QA sign-off before publish.')], {
memory: { thread: docThreadId, resource: docResourceId },
})

<turn> 标签会保留在消息正文中,因此在后续轮次召回历史时,模型仍然能看到每句话是谁说的。

与 Memory 层结合使用
与 Memory 层结合使用的直接链接

用户标记模式可以与每个 Memory 层组合使用。请根据对话需要记住每位用户相关事实的时长选择对应层:

  • 短对话(单个会话或规模足够小、能够装入 lastMessages 的 Thread),或者需要逐字记录谁说了什么时:只使用消息历史即可。历史中的用户标签已经足够,无需额外的 Memory 层。
  • 长期运行的 Thread(对话规模超过 lastMessages,而且每位用户的事实需要在历史淘汰后继续保留):使用 Observational Memory
  • 需要结构化参与者列表,或者你的 Storage Adapter 不支持 OM(OM 需要 LibSQL、PG 或 MongoDB):使用 Working Memory

建议使用 Observational Memory 或 Working Memory 二者之一,因为它们覆盖的需求存在重叠。同时运行两者会增加延迟和 token 成本,却没有太大收益。

仅使用消息历史
仅使用消息历史的直接链接

对于短对话或需要逐字记录谁说了什么的场景,历史中的用户标签已经足够。lastMessages 会将先前轮次带回上下文,同时保留其归属信息:

src/mastra/agents/collab-basic.ts
import { Memory } from '@mastra/memory'
import { LibSQLStore } from '@mastra/libsql'

const memory = new Memory({
storage: new LibSQLStore({ id: 'collab-storage', url: 'file:./collab.db' }),
options: {
lastMessages: 20,
},
})

模型会从当前消息的 <turn> 标签,以及通过 lastMessages 带回的先前已标记消息中读取身份。

Observational Memory(OM)会将每位用户的事实提取到后台日志中,而不会占用 Agent 的 Tool 预算。默认 Observer 模型能够直接读取 <turn> 标签,并生成类似 Alice stated her favorite color is teal.Bob asked for QA sign-off before publish. 的归属记录。

如果你的 Storage 支持 OM,在多用户 Thread 中应优先使用 OM,而不是 Working Memory。OM 会自动提取事实,可以扩展到任意数量的参与者,也不需要维护模板。不做任何覆盖即可启用:

src/mastra/agents/collab-om.ts
import { Memory } from '@mastra/memory'
import { LibSQLStore } from '@mastra/libsql'

const memory = new Memory({
storage: new LibSQLStore({ id: 'collab-storage', url: 'file:./collab.db' }),
options: {
lastMessages: 20,
observationalMemory: true,
},
})

OM 需要使用支持它的 Storage Adapter:@mastra/libsql@mastra/pg@mastra/mongodb@mastra/oracledb

备注

如果将 Observer 切换到能力较弱的模型后,发现事实都被归为笼统的 User,请使用 observation.instruction 教 Observer 读取 <turn> 标签。

使用 Working Memory
使用 Working Memory的直接链接

当 OM 不可用时(例如 Storage Adapter 不支持 OM),或者需要 Agent 在每一轮都能读写结构化且确定的参与者列表时,请使用 Working Memory。

默认的 Working Memory 模板假定每个 Thread 只有一个用户(“First Name”“Last Name”等)。对于多用户 Thread,请提供一个包含参与者列表的模板:

src/mastra/agents/collab-wm.ts
import { Memory } from '@mastra/memory'
import { LibSQLStore } from '@mastra/libsql'

const memory = new Memory({
storage: new LibSQLStore({ id: 'collab-storage', url: 'file:./collab.db' }),
options: {
lastMessages: 20,
workingMemory: {
enabled: true,
scope: 'thread',
template: `# Document Collaboration State

## Participants
<!-- One entry per known collaborator. Use author_id as the stable key. -->
<!-- - **<author_name>** (<author_id>, <functional_role>): <their position> -->

## Open Questions

## Decisions
`,
},
},
})

设置 scope: 'thread',让参与者列表属于文档,而不是属于某个用户。再添加一条指令,要求 Agent 每当新的 author_id 出现在 <turn> 中时,就将新参与者追加到列表中。

有关模板的更多信息,请参阅自定义模板

安全性
安全性的直接链接

请从经过身份验证的请求上下文中设置 speaker,绝不要从请求正文中设置。如果客户端可以自行选择 author_id,某个用户就可能冒充另一个用户。请使用 Request Context 从身份验证层读取已验证用户,并在调用 Agent 之前在服务端构建 <turn> 标签。