AI智能体对话平台开发实战(四):用户之心——多智能体身份绑定系统
本文聚焦多智能体身份绑定系统的设计与实现,从隐私保护与场景适配的双重需求出发,提出“每个智能体独立保存用户身份”的核心设计理念——用户告诉医生的病情不会被理财顾问知晓,同时避免每次切换智能体重复杂输入。通过灵活的JSON键值对结构,支持任意自定义字段,满足不同场景下的身份差异化需求。后端实现身份信息的读写接口,并将身份数据动态注入system prompt,让AI在对话中自然感知用户姓名、职业等信
📖 引言
在上一篇中,我们让智能体真正“会说话”了。现在,当你点开“助手小M”,发送“我叫张三”,它会回复“你好张三,很高兴认识你”。一切看起来都很美好。
但问题来了:当你切换到“鲁迅”智能体,再问“我叫什么”,它会回答什么?
按照目前的实现,它会说“我不知道你叫什么”。因为每个智能体的对话历史是独立的,你在“助手小M”那里告诉的信息,“鲁迅”并不知道。
这合理吗?
从隐私角度看,这很合理——你不希望每个AI都知道你的所有信息。你告诉医生的病情,不希望理财顾问知道;你告诉理财顾问的收入,不希望心理医生知道。
但从用户体验角度看,这又很麻烦——每次换一个新智能体,都要重新自我介绍一遍:“我叫张三,今年25岁,是一名程序员…”
有没有一种两全其美的方案?既保护隐私,又减少重复输入?
这就是本篇要解决的问题:多智能体身份绑定系统。让每个智能体独立保存用户信息,用户可以针对不同智能体填写不同的身份,实现真正的“千人千面”。

🎯 本章目标
学完本篇,你将能够:
- 设计合理的用户身份数据结构
- 为每个智能体独立存储身份信息
- 将身份信息动态注入对话上下文
- 实现前端身份编辑界面
- 理解身份隔离的设计价值
- 思考身份信息与隐私保护的平衡
💭 第一部分:身份系统的设计哲学
从现实生活找灵感
想象一下你在现实世界中的身份:
- 在医生面前,你是“患者”,需要告诉病情、病史
- 在老师面前,你是“学生”,需要知道你的学习情况
- 在朋友面前,你是“张三”,可以聊兴趣爱好
- 在网上购物,你是“收货人”,需要姓名、电话、地址
你会在医生面前聊你的网购收货地址吗?不会。你会在购物网站填你的病历吗?更不会。
每个人在不同场景下,展示的是不同的侧面。 这才是真实的人性。
身份信息的本质
身份信息本质上是一组键值对:
{
"name": "张三",
"age": 25,
"gender": "男",
"occupation": "程序员",
"description": "喜欢读书、 coding"
}
但不同场景需要的字段不同:
- 医疗场景:需要病史、过敏史
- 教育场景:需要学历、专业
- 购物场景:需要地址、电话
所以身份信息的结构应该是灵活的、可扩展的,而不是固定的表结构。
身份隔离的价值
为什么要为每个智能体独立保存身份?
价值一:隐私保护
用户告诉A智能体的信息,B智能体不知道。这是最基本的隐私边界。
价值二:场景适配
同一个用户,在不同智能体面前可以有不同的“人设”。面对鲁迅,你可以是“一个热爱文学的读者”;面对凌云,你可以是“一个请教技术的程序员”。
价值三:关系维护
智能体和用户之间的关系,应该是“一对一”的。你和每个朋友的记忆都是独立的,不会混在一起。智能体也应该如此。
📦 第二部分:数据结构设计
文件存储位置
按照“文件夹即智能体”的理念,身份信息当然也应该放在智能体的文件夹里:
助手小M/
├── 助手小M.png
├── 助手小M.txt
├── chat.json
└── identity.json # 用户身份信息
思考:为什么叫identity.json而不是user.json?因为“identity”更能体现这是“用户在这个智能体面前的形象”,而不是用户的全局身份。
数据结构设计
最简单的设计:
{
"name": "张三",
"age": "25",
"gender": "男",
"occupation": "程序员",
"description": "喜欢读书、 coding"
}
但这太死板了。如果用户想填“兴趣爱好”、“所在城市”,怎么办?
更灵活的设计:允许任意字段
{
"name": "张三",
"age": "25",
"gender": "男",
"occupation": "程序员",
"city": "北京",
"hobby": "读书、 coding",
"custom_field": "任意值"
}
极致灵活的设计:元数据格式
{
"basic": {
"name": "张三",
"age": "25",
"gender": "男"
},
"extended": {
"occupation": "程序员",
"city": "北京"
},
"preferences": {
"language": "中文",
"response_style": "简洁"
}
}
思考:哪种设计更好?取决于需求。初期用最简单的方式,后续需要再扩展。YAGNI原则——你不会需要它(You Ain‘t Gonna Need It)。
与对话上下文的融合
有了身份信息,怎么让它发挥作用?答案是注入到system prompt中。
原始的system prompt:
你是助手小M,一个友好热情的AI助手...
注入身份信息后:
用户信息:姓名张三,年龄25岁,职业程序员,个人描述喜欢读书、coding。
你是助手小M,一个友好热情的AI助手...
这样AI从一开始就知道用户是谁,回答时可以自然地称呼用户的名字,引用用户的职业信息。
思考:身份信息应该放在system prompt的开头还是结尾?开头更早被AI注意到,结尾可能会被长对话冲淡。所以放在开头更合理。
🔨 第三部分:后端接口实现
读取身份信息
// 获取用户身份信息
app.get('/api/identity/:agentName', (req, res) => {
const { agentName } = req.params;
try {
const identityPath = path.join(__dirname, agentName, 'identity.json');
if (!fs.existsSync(identityPath)) {
return res.json({ success: true, identity: null });
}
const data = fs.readFileSync(identityPath, 'utf8');
const identity = JSON.parse(data);
res.json({ success: true, identity });
} catch (error) {
console.error('读取身份信息失败:', error);
res.status(500).json({
success: false,
error: '读取身份信息失败'
});
}
});
设计思考:
- 文件不存在时返回
identity: null,而不是404错误。这样前端可以区分“未设置”和“出错”两种状态。 - 出错时返回500,让前端知道是服务器问题。
保存身份信息
// 保存用户身份信息
app.post('/api/identity/:agentName', (req, res) => {
const { agentName } = req.params;
const identity = req.body;
try {
const agentDir = path.join(__dirname, agentName);
// 确保智能体目录存在(理论上应该存在,但以防万一)
if (!fs.existsSync(agentDir)) {
fs.mkdirSync(agentDir, { recursive: true });
}
const identityPath = path.join(agentDir, 'identity.json');
fs.writeFileSync(identityPath, JSON.stringify(identity, null, 2));
res.json({ success: true });
} catch (error) {
console.error('保存身份信息失败:', error);
res.status(500).json({
success: false,
error: '保存身份信息失败'
});
}
});
关键点:
- 直接接受前端传来的任意JSON,不做字段校验。这是故意为之——让身份信息灵活可扩展。
- 写入时用
null, 2格式化,方便用户手动查看和修改。
身份信息注入对话
改造第三篇的对话API,加入身份信息:
app.post('/api/chat', async (req, res) => {
const { agentName, message } = req.body;
try {
// 1. 读取智能体prompt
const promptPath = path.join(__dirname, agentName, `${agentName}.txt`);
let systemPrompt = fs.readFileSync(promptPath, 'utf8');
// 2. 读取身份信息
const identityPath = path.join(__dirname, agentName, 'identity.json');
if (fs.existsSync(identityPath)) {
const identity = JSON.parse(fs.readFileSync(identityPath, 'utf8'));
// 构建身份信息文本
let identityText = '用户信息:';
const validFields = [];
if (identity.name) validFields.push(`姓名${identity.name}`);
if (identity.age) validFields.push(`年龄${identity.age}`);
if (identity.gender) validFields.push(`性别${identity.gender}`);
if (identity.occupation) validFields.push(`职业${identity.occupation}`);
if (identity.description) validFields.push(`个人描述${identity.description}`);
// 如果有其他自定义字段,也加进去
Object.entries(identity).forEach(([key, value]) => {
if (!['name', 'age', 'gender', 'occupation', 'description'].includes(key)) {
if (value) validFields.push(`${key}${value}`);
}
});
if (validFields.length > 0) {
identityText += validFields.join(',') + '。\n';
// 将身份信息插入到system prompt最前面
systemPrompt = identityText + systemPrompt;
}
}
// 3. 后续逻辑保持不变...
// 读取聊天记录、构建messages、调用API、保存回复
} catch (error) {
// 错误处理
}
});
设计思考:
- 为什么用
if (identity.name)而不是if (identity.name !== '')?因为空字符串也是有效值?不,空字符串应该被视为未填写。所以用if (identity.name)可以同时过滤undefined、null和空字符串。 - 自定义字段的处理:把所有不在预设字段列表中的字段都当作自定义字段处理,给了用户极大的灵活性。
🖥️ 第四部分:前端身份编辑界面
界面设计思路
身份编辑界面应该:
- 简洁:不要一上来就展示一大堆字段
- 可扩展:用户可以添加自定义字段
- 上下文相关:知道是为哪个智能体编辑身份
对话框设计
在index.html中添加身份编辑对话框:
<div id="identityDialog" class="dialog" style="display: none;">
<div class="dialog-content">
<div class="dialog-header">
<h3>我的身份 - <span id="identityAgentName"></span></h3>
<button id="closeDialogBtn" class="close-btn">×</button>
</div>
<div class="dialog-body">
<div class="form-group">
<label>姓名</label>
<input type="text" id="identityName" placeholder="请输入您的姓名">
</div>
<div class="form-group">
<label>年龄</label>
<input type="number" id="identityAge" placeholder="请输入您的年龄">
</div>
<div class="form-group">
<label>性别</label>
<select id="identityGender">
<option value="">请选择</option>
<option value="男">男</option>
<option value="女">女</option>
<option value="其他">其他</option>
</select>
</div>
<div class="form-group">
<label>职业</label>
<input type="text" id="identityOccupation" placeholder="请输入您的职业">
</div>
<div class="form-group">
<label>个人描述</label>
<textarea id="identityDescription" placeholder="请输入您的个人描述" rows="3"></textarea>
</div>
<!-- 自定义字段区域(预留扩展) -->
<div id="customFields"></div>
<button id="addCustomFieldBtn" class="btn-link">+ 添加自定义字段</button>
</div>
<div class="dialog-footer">
<button id="saveIdentityBtn" class="btn-primary">保存</button>
<button id="cancelIdentityBtn" class="btn-secondary">取消</button>
</div>
</div>
</div>
打开对话框的逻辑
let currentAgent = null; // 全局变量,记录当前选中的智能体
function showIdentityDialog() {
if (!currentAgent) {
alert('请先选择一个智能体');
return;
}
// 显示对话框
document.getElementById('identityDialog').style.display = 'flex';
document.getElementById('identityAgentName').textContent = currentAgent.name;
// 加载已保存的身份信息
loadIdentity(currentAgent.name);
}
async function loadIdentity(agentName) {
try {
const response = await fetch(`/api/identity/${agentName}`);
const data = await response.json();
if (data.success && data.identity) {
// 填充表单
document.getElementById('identityName').value = data.identity.name || '';
document.getElementById('identityAge').value = data.identity.age || '';
document.getElementById('identityGender').value = data.identity.gender || '';
document.getElementById('identityOccupation').value = data.identity.occupation || '';
document.getElementById('identityDescription').value = data.identity.description || '';
// 处理自定义字段
renderCustomFields(data.identity);
} else {
// 清空表单
document.getElementById('identityName').value = '';
document.getElementById('identityAge').value = '';
document.getElementById('identityGender').value = '';
document.getElementById('identityOccupation').value = '';
document.getElementById('identityDescription').value = '';
document.getElementById('customFields').innerHTML = '';
}
} catch (error) {
console.error('加载身份信息失败:', error);
alert('加载身份信息失败');
}
}
保存身份信息
async function saveIdentity() {
// 收集基础字段
const identity = {
name: document.getElementById('identityName').value.trim(),
age: document.getElementById('identityAge').value.trim(),
gender: document.getElementById('identityGender').value,
occupation: document.getElementById('identityOccupation').value.trim(),
description: document.getElementById('identityDescription').value.trim()
};
// 收集自定义字段
const customFields = document.querySelectorAll('.custom-field');
customFields.forEach(field => {
const key = field.querySelector('.custom-key').value.trim();
const value = field.querySelector('.custom-value').value.trim();
if (key && value) {
identity[key] = value;
}
});
try {
const response = await fetch(`/api/identity/${currentAgent.name}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(identity)
});
const data = await response.json();
if (data.success) {
alert('身份信息保存成功!');
hideIdentityDialog();
} else {
alert('保存失败,请重试');
}
} catch (error) {
console.error('保存身份信息失败:', error);
alert('保存失败,请检查网络');
}
}
🔄 第五部分:身份信息的动态感知
在对话中感知身份
有了身份信息后,AI应该能够自然地感知和使用这些信息。比如:
用户:你好
AI:你好张三,今天想聊点什么?
用户:我今年多大了?
AI:根据你之前的信息,你今年25岁。
用户:我做什么工作的?
AI:你是一名程序员。
这些都不需要用户重复告诉AI,而是从身份信息中自动获取。
如何让AI“记住”身份信息?
身份信息只是注入到system prompt中,并没有作为对话历史存储。这意味着:
- AI在每一轮对话开始时都知道用户身份
- 但AI不会在回复中主动提及身份信息(除非必要)
思考:如果用户修改了身份信息(比如改了名字),AI会立刻感知到吗?会的,因为下一轮对话的system prompt已经更新了。
🧪 第六部分:测试与验证
测试用例
用例1:首次设置身份
- 选择一个智能体
- 点击“我的身份”,填写信息并保存
- 发送消息,看AI是否使用你的名字
用例2:切换智能体
- 在智能体A设置身份
- 切换到智能体B,点击“我的身份”,应该看到空表单
- 为智能体B设置不同身份
- 分别和两个智能体对话,看他们是否使用各自记住的名字
用例3:修改身份
- 修改已保存的身份信息
- 发送新消息,看AI是否使用更新后的信息
用例4:自定义字段
- 添加自定义字段“城市-北京”
- 保存后问“我在哪个城市”,看AI是否能回答
边界情况处理
| 场景 | 预期行为 |
|---|---|
| 未设置身份 | AI不提及用户信息,正常对话 |
| 部分字段为空 | 只使用有值的字段 |
| 字段值很长 | 正常注入,注意不要超过token限制 |
| 特殊字符 | JSON会自动转义,没问题 |
💭 第七部分:进阶思考
思考1:身份信息的版本管理
如果用户多次修改身份信息,旧的身份信息会被覆盖。但对话历史里可能还包含旧的称呼。比如用户原来叫“张三”,后来改成“李四”,之前的对话里AI都叫“张三”,现在突然改口叫“李四”,会不会很奇怪?
解决方案:
- 在修改身份时,可以添加一条系统消息:“用户已将姓名从张三改为李四”
- 让AI在后续对话中自然过渡
思考2:身份信息的继承
有些信息可能是通用的,比如用户的母语、时区、偏好等。是不是每个智能体都要单独设置?
解决方案:可以设计一个“全局身份”和“智能体身份”的继承机制。全局身份是所有智能体共享的基础信息,智能体身份是覆盖或扩展。
思考3:身份信息的导入导出
用户可能在多个设备上使用,或者想备份自己的身份信息。提供导入导出功能会很有价值。
// 导出所有身份信息
async function exportAllIdentities() {
const agents = await getAgents();
const identities = {};
for (const agent of agents) {
const resp = await fetch(`/api/identity/${agent.name}`);
const data = await resp.json();
if (data.identity) {
identities[agent.name] = data.identity;
}
}
// 下载为JSON文件
const blob = new Blob([JSON.stringify(identities, null, 2)],
{ type: 'application/json' });
// 触发下载...
}
思考4:隐私与透明的平衡
身份信息存储在本地文件,理论上很安全。但如果用户在多台设备间同步文件夹,就需要考虑文件传输的安全性。
建议:在文档中明确告知用户身份信息的存储位置和安全性,让用户自己决定是否在云端同步。
思考5:身份信息的上下文窗口管理
如果身份信息很长(比如详细的自传),会占用大量token。是不是每次都要完整注入?
优化方案:
- 只注入最近使用的字段
- 将身份信息作为单独的上下文块,对话历史截断时保留身份信息
- 允许用户标记哪些字段是“重要的”,哪些可以省略
🎯 本篇小结
在这一篇中,我们为MultiMind注入了“用户之心”:
| 任务 | 成果 |
|---|---|
| 身份数据结构设计 | 灵活的键值对格式,支持扩展 |
| 文件存储 | 每个智能体独立的identity.json |
| 后端接口 | 读取和保存身份信息的API |
| 身份注入 | 将身份信息动态融入system prompt |
| 前端界面 | 完整的身份编辑对话框 |
| 自定义字段 | 支持用户添加任意字段 |
核心收获:
- 身份隔离是隐私保护的基础,也是“千人千面”的前提
- 身份信息应该灵活可扩展,而不是固定的表结构
- 身份信息通过注入system prompt发挥作用
- 不同智能体可以有完全不同的用户画像
🔮 下篇预告
第五篇:数据之基——文件系统数据库设计与并发控制
在下一篇中,我们将深入数据层面:
- JSON文件作为数据库的优缺点分析
- 读写锁与并发控制策略
- 数据备份与恢复机制
- 性能优化与扩展思考
敬请期待!
📝 写在最后
身份系统让MultiMind从一个“会说话的AI”变成了一个“懂你的AI”。每个智能体面前,用户都可以是不同的样子——这不仅是技术实现,更是一种设计哲学:尊重用户的隐私,也尊重用户的多样性。
回顾前三篇:
- 第一篇:思考架构,画出蓝图
- 第二篇:搭建地基,实现智能体发现
- 第三篇:注入对话之心,让AI会说话
- 第四篇:赋予用户之心,让AI懂身份
我们的MultiMind已经初具雏形。下一篇,我们将深入底层,看看这些数据是如何被安全、高效地管理的。
思考题:如果你来设计身份系统,你会支持“身份模板”吗?比如“程序员模板”包含技术栈、项目经验,“学生模板”包含学校、专业。这样的设计有什么利弊?欢迎在评论区分享你的思考。
更多推荐


所有评论(0)