📖 引言

在上一篇中,我们让智能体真正“会说话”了。现在,当你点开“助手小M”,发送“我叫张三”,它会回复“你好张三,很高兴认识你”。一切看起来都很美好。

但问题来了:当你切换到“鲁迅”智能体,再问“我叫什么”,它会回答什么?

按照目前的实现,它会说“我不知道你叫什么”。因为每个智能体的对话历史是独立的,你在“助手小M”那里告诉的信息,“鲁迅”并不知道。

这合理吗?

从隐私角度看,这很合理——你不希望每个AI都知道你的所有信息。你告诉医生的病情,不希望理财顾问知道;你告诉理财顾问的收入,不希望心理医生知道。

但从用户体验角度看,这又很麻烦——每次换一个新智能体,都要重新自我介绍一遍:“我叫张三,今年25岁,是一名程序员…”

有没有一种两全其美的方案?既保护隐私,又减少重复输入?

这就是本篇要解决的问题:多智能体身份绑定系统。让每个智能体独立保存用户信息,用户可以针对不同智能体填写不同的身份,实现真正的“千人千面”。

获取项目源代码 Gitee MultiMind 雪豹同志

在这里插入图片描述


🎯 本章目标

学完本篇,你将能够:

  • 设计合理的用户身份数据结构
  • 为每个智能体独立存储身份信息
  • 将身份信息动态注入对话上下文
  • 实现前端身份编辑界面
  • 理解身份隔离的设计价值
  • 思考身份信息与隐私保护的平衡

💭 第一部分:身份系统的设计哲学

从现实生活找灵感

想象一下你在现实世界中的身份:

  • 在医生面前,你是“患者”,需要告诉病情、病史
  • 在老师面前,你是“学生”,需要知道你的学习情况
  • 在朋友面前,你是“张三”,可以聊兴趣爱好
  • 在网上购物,你是“收货人”,需要姓名、电话、地址

你会在医生面前聊你的网购收货地址吗?不会。你会在购物网站填你的病历吗?更不会。

每个人在不同场景下,展示的是不同的侧面。 这才是真实的人性。

身份信息的本质

身份信息本质上是一组键值对

{
  "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)可以同时过滤undefinednull和空字符串。
  • 自定义字段的处理:把所有不在预设字段列表中的字段都当作自定义字段处理,给了用户极大的灵活性。

🖥️ 第四部分:前端身份编辑界面

界面设计思路

身份编辑界面应该:

  1. 简洁:不要一上来就展示一大堆字段
  2. 可扩展:用户可以添加自定义字段
  3. 上下文相关:知道是为哪个智能体编辑身份

对话框设计

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:首次设置身份

  1. 选择一个智能体
  2. 点击“我的身份”,填写信息并保存
  3. 发送消息,看AI是否使用你的名字

用例2:切换智能体

  1. 在智能体A设置身份
  2. 切换到智能体B,点击“我的身份”,应该看到空表单
  3. 为智能体B设置不同身份
  4. 分别和两个智能体对话,看他们是否使用各自记住的名字

用例3:修改身份

  1. 修改已保存的身份信息
  2. 发送新消息,看AI是否使用更新后的信息

用例4:自定义字段

  1. 添加自定义字段“城市-北京”
  2. 保存后问“我在哪个城市”,看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
前端界面 完整的身份编辑对话框
自定义字段 支持用户添加任意字段

核心收获

  1. 身份隔离是隐私保护的基础,也是“千人千面”的前提
  2. 身份信息应该灵活可扩展,而不是固定的表结构
  3. 身份信息通过注入system prompt发挥作用
  4. 不同智能体可以有完全不同的用户画像

🔮 下篇预告

第五篇:数据之基——文件系统数据库设计与并发控制

在下一篇中,我们将深入数据层面:

  • JSON文件作为数据库的优缺点分析
  • 读写锁与并发控制策略
  • 数据备份与恢复机制
  • 性能优化与扩展思考

敬请期待!


📝 写在最后

身份系统让MultiMind从一个“会说话的AI”变成了一个“懂你的AI”。每个智能体面前,用户都可以是不同的样子——这不仅是技术实现,更是一种设计哲学:尊重用户的隐私,也尊重用户的多样性

回顾前三篇:

  • 第一篇:思考架构,画出蓝图
  • 第二篇:搭建地基,实现智能体发现
  • 第三篇:注入对话之心,让AI会说话
  • 第四篇:赋予用户之心,让AI懂身份

我们的MultiMind已经初具雏形。下一篇,我们将深入底层,看看这些数据是如何被安全、高效地管理的。


思考题:如果你来设计身份系统,你会支持“身份模板”吗?比如“程序员模板”包含技术栈、项目经验,“学生模板”包含学校、专业。这样的设计有什么利弊?欢迎在评论区分享你的思考。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐