跳到主要内容
非官方简体中文翻译,内容版权归 Mastra 所有,遵循 Apache-2.0 许可。

Multi-user threads

单个 Mastra thread 可以由多个用户共享,每个用户都有自己的姓名和职能角色。你将说话者身份携带在消息正文中,这样 agent 在读取单个共享 thread 时就能区分不同用户。

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

当多个人通过一个 agent 就同一主题协作时,请使用多用户 threads:

  • 包含编辑、审阅者和批准者的协作文档
  • 一个 assistant 服务多个参与者的群聊
  • 不同角色拥有不同权限的多方评审

在所有参与者之间共享一个 resourceId
在所有参与者之间共享一个-resourceid的直接链接

一个 thread 只属于一个 resourceId,因此共享 thread 上的所有参与者都需要传入相同的值。不要使用用户 id(单用户应用的默认值),而是以对话本身作为 resourceId 的键,例如共享文档使用 doc_${docId},群聊使用 room_${roomId}。所有人指向相同的 resourceId 后,他们读写的就是相同的历史。

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

模型需要知道每一轮是谁在说话。由于消息正文是唯一能保留到历史并返回上下文的地方,请将每条用户消息包装在一个带有说话者 id、姓名和角色的小型 <turn> 标签中。该标签会附加在消息上,因此当召回先前的轮次时,模型仍能看到谁说了什么。

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

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>`,
}
}

在 instructions 中教会 agent 如何读取 <turn> 标签。Agent 必须配置 memory,这样才能以 thread 和 resource 调用:

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。每个参与者共享相同的 thread 和 resource:

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 层。
  • 长时间运行的 threads(超出 lastMessages、需要每用户事实在历史淘汰后仍然保留的对话):使用观察式记忆。
  • 需要结构化参与者列表,或你的存储适配器不支持 OM(OM 需要 LibSQL、PG 或 MongoDB):使用工作记忆。

我们建议使用观察式记忆或工作记忆之一,因为它们满足重叠的需求。同时运行两者会增加延迟和 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(推荐)
使用 Observational Memory(推荐)的直接链接

Observational Memory (OM) 将每用户事实提取到后台日志中,而不会消耗 agent 的工具预算。默认的 Observer 模型原生读取 <turn> 标签,并生成类似 Alice stated her favorite color is teal. 和 Bob asked for QA sign-off before publish. 的归属信息。

当你的存储支持时,多用户 threads 优先使用 OM 而不是工作记忆。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 需要支持它的存储适配器:@mastra/libsql、@mastra/pg、@mastra/mongodb、@mastra/mysql、@mastra/convex 或 @mastra/oracledb。

备注

如果你将 Observer 切换为能力较弱的模型,发现事实被归属到通用的 User 而不是各个参与者,请使用 observation.instruction 说明如何解释 <turn> 标签。

使用工作记忆
使用工作记忆的直接链接

当 OM 不是选项时使用工作记忆,例如你的存储适配器不支持 OM,或你需要一个 agent 每轮都能读取和写入的结构化、确定性的参与者列表。

默认的工作记忆模板假设每个 thread 一个用户("First Name"、"Last Name" 等)。对于多用户 threads,请提供带参与者列表的模板:

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 每当 <turn> 中出现新的 author_id 时,就将新参与者追加到列表中。

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

安全
安全的直接链接

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

相关内容
相关内容的直接链接