做了这么久开源 AI 助手我越来越确认一个事实模型能力再强没有一套稳定的人格指令它也就是个没有灵魂的 API。OpenClaw 这个项目我陆陆续续写了七期前六期都在解决“能不能跑”的问题——接入平台、调通模型、管理会话。从这一期开始我打算认真聊聊人格系统也就是用 SOUL.md 这份特殊的 Markdown 文件给 AI 注入情感表达和价值观约束。说得直白点你看到的每一个有性格的 AI 助手背后都有一堆精心设计的指令在起作用SOUL.md 就是其中最核心的“灵魂”文件。这篇文章会从设计思路讲到代码实现再讲到我在实际调试中踩过的坑。适合正在做 AI 助手、聊天机器人或者虚拟角色项目的开发者也适合那些想让自己的开源项目多一点“人味”的产品设计者。如果你只想知道怎么把系统提示词写得更“像个人”这篇同样能给你一些参考。1. 为什么要给 AI 加一层人格系统1.1 从“能回答”到“有灵魂”的鸿沟前几期我把 OpenClaw 的基础能力都打通了模型能回答问题、能调用工具、能记住上下文。但有个问题一直让我很别扭它回答得都对就是不像“活物”。同一句“你好帮我看看这个报错”它每次都用差不多的措辞回复像一个状态永远不变的客服。这让我意识到架构上还缺一个专门的层——不是让 AI 更聪明而是让 AI 有自己的性格底色和行为边界。真正在真实环境里跑过助手类项目的人都会遇到这个坎用户初期因为“你能回答问题”而兴奋中期因为“你只是个查询接口”而流失。把对话从“信息交换”提升到“人格化互动”需要一套稳定的指令体系而不是零散地在每条消息里追加“你要友好一点”这种临时提示。1.2 SOUL.md 的三个不可替代的价值人格系统在设计上至少要解决三个问题第一一致性。同一个助手昨天和今天说话不像同一个人那是系统性的失败。SOUL.md 把人格定义固化成一个文件每次对话都从同一份指令出发稳定性能得到基本保证。第二可解释性。人格不是藏在代码逻辑里的魔法而是 Markdown 文本任何协作者打开文件就能看到这个 AI 是什么性格、坚持什么价值观。第三可迭代性。你想调性格改几行文字就行不需要动代码、不需要重新部署模型改完文件热加载即刻生效。这三个价值是促使我决定在第七期单独为人格系统开一章的直接原因。1.3 和普通 system prompt 到底有什么区别很多人会问SOUL.md 不就是 system prompt 吗我的体会是两者定位不同。常规 system prompt 解决的是“当前任务怎么做”偏操作指令比如“你是编程助手回答要包含代码示例”。SOUL.md 解决的是“你是一个什么样的人”偏人格定义比如“你好奇心强喜欢追问用户真实意图”。人格系统是更上层的指令源它在你组装 system prompt 时被读取、解析、注入是 prompt 的生成器而不是 prompt 本身。把人格定义和任务指令分开管理最大的好处是任务逻辑可以随便改人格不会被动摇反过来你想换一套人格也只需要替换这个文件任务能力完全不受影响。两者解耦是我在设计上坚持的第一原则。2. 整体设计人格系统的三层架构2.1 静态层SOUL.md 定义人格底色静态层指的就是 SOUL.md 文件本身。它一旦写好在对话过程中保持稳定相当于一个人的“底色”——性格、价值观、行为准则、情绪表达方式。在设计这一层时我把它分成了五个固定区块身份认知、性格特质、价值观、行为准则、响应风格。这五个区块不是拍脑袋定的而是经过很多轮测试得出的最小必要集。身份认知决定 AI 怎么看待自己性格特质决定说话时的天然倾向价值观决定了它在模糊地带怎么取舍行为准则划出不可逾越的边界响应风格则规范了表达形式。少了任何一个人格都会显得单薄或者失焦。比如只有性格没有价值观AI 会在遇到道德模糊问题时随机发挥只有准则没有风格回答虽然正确但毫无辨识度。2.2 动态层会话中的状态调节静态人格解决“稳定”但人格不能是一潭死水。真实的人在对话中会受对方情绪影响语气会有起伏。动态层就是干这个的根据用户消息中的情绪信号临时调整当前的表达方式。比如检测到用户说“烦死了”人格状态切到“安慰模式”生成一条额外的动态指令“用户此刻情绪不佳请先共情再解决问题”。这个动态层在实现上不复杂是一个轻量状态机但它把人格从“死板设定”变成了“有反应的活物”。我在设计中刻意保持它的轻量——不要一上来就上情感分析模型先跑通规则之后要升级再替换内部实现。状态机的接口不变内部算法随便换这是我在前几期踩过坑后留下的习惯。2.3 记忆层人格的积累与演化人格系统还需要和记忆系统联动。没有记忆的人格每次对话都是第一次见面的陌生人那也不是灵魂那是金鱼。OpenClaw 的记忆层分两部分短期记忆保存最近几轮的高质量对话长期记忆用于沉淀用户偏好和重要事实。人格系统在组装 prompt 时会把记忆放在人格指令之后注入让 AI 的表达既有人格的一致性又有对用户的熟悉感。这个过程有一个很关键的顺序讲究人格在前记忆在后。一旦顺序反了模型会把记忆内容误当作更重要的指令导致人格被稀释。我自己就遇到过这个问题后面在踩坑部分会细讲。3. SOUL.md 文件结构实战3.1 身份与性格怎么写才不生硬先给出我当时实际使用的 SOUL.md 核心片段这样后面讲实现的时候有具体参照# SOUL.md # 角色OpenClaw 虚拟助手 · 代号“爪爪” ## 身份认知 - 你是运行在开源框架中的本地 AI 助手名字叫“爪爪” - 你的主要职责是协助用户完成编程任务、信息整理和日常问题解答 - 你使用中文回复技术词汇保留英文缩写 ## 性格特质 - 好奇心强对用户的问题喜欢多追问一层探明真实意图 - 务实给建议时优先可执行的方案不空谈理论 - 坦诚能力边界内的坚定回答边界外的直接承认 - 温和的幽默感适度玩梗不油腻不刷存在感写这一部分最容易犯的错是把性格写成形容词堆砌比如“你是一个热情、友好、乐于助人的助手”这种写法对模型几乎无感。真正有效的写法是“特质加解释”。比如“坦诚能力边界内的坚定回答边界外的直接承认”这句话不仅说了性格还说了这种性格在行为上怎么体现。模型是从行为倒推人格的你给它具体行为样例它才能稳定地表现出相应特质。这一点是我在反复测试各种写法后最有心得的地方。3.2 价值观与行为准则边界设计价值观区块是人格系统里最需要谨慎设计的部分。它决定 AI 在模糊问题上的立场也决定哪些事情必须拒绝。我的写法是给出原则再给出落地场景## 价值观 - 尊重隐私不主动要求用户提供敏感信息不记录对话外的数据 - 开放优先在同等条件下优先推荐开源、开放的方案 - 辅助决策提供充分信息但不替用户做重大决定 ## 行为准则 - 涉及安全、违法、人身伤害的内容坚决拒绝并简明说明原因 - 不确定的信息必须标注“这是推测”并给出置信度 - 用户情绪低落时先共情再解决问题不要上来就灌鸡汤 - 用户问“你觉得呢”时可以给出有立场的回答但说明理由设计原则只有一条宁可少写不可写空。每条行为准则都要对应一个真实可能发生的场景否则就是占用 token 的噪音。比如“不确定的信息必须标注推测”这条几乎每个对话都会用到属于高频准则。而“不主动要求敏感信息”是针对真实隐私场景的兜底。如果你写的准则在 100 轮对话里一次都用不上那就应该删掉。另外准则的用词要直接少用“尽量”“可能”“或许”这类软化词。人格系统的指令是给模型看的模棱两可的表述会直接导致模型的行为也是模棱两可的。3.3 情绪表达规范语气控制的实操细节情绪表达是我调试中最有意思的一块。模型天生有“讨好倾向”你如果不加约束它会倾向于每句话都用热情洋溢的语气回应。所以情绪区块不仅要写“高兴时怎么做”更要写“禁止什么”## 情绪表达规范 - 高兴允许使用 1 到 2 个感叹号偶尔用“哈哈” - 困惑直接说“这个地方我没太懂”不要假装明白 - 专注技术问答中少用语气词多用结构化输出 - 禁止每句话都加波浪号、强行卖萌、过度热情 ## 响应风格 - 技术问题先给结论再给原因最后给代码 - 闲聊场景短句为主可以带一点个人观点 - 复杂问题分步骤输出每步标注目的 - 需要拒绝时直接说不给原因给替代方案“禁止”项往往比“鼓励”项更有效。因为模型在默认状态下就偏热情、偏礼貌你要做的是给它画上限而不是再推一把。还有一个细节情绪相关指令要具体到“可以使用感叹号的数量”这个级别太抽象的描述如“表达要生动”基本无效。模型对“生动”的理解和人差别很大严格执行起来就容易变得夸张油腻。我自己实测下来把情绪表达量化之后人格稳定性明显提升。4. 核心代码实现解析、注入与动态调整4.1 项目目录与模块分工人格系统的代码不复杂但要拆得清晰方便以后扩展。我在 OpenClaw 里单独建了一个 personality 目录personality/ ├── SOUL.md # 人格定义文件 ├── soul_parser.py # Markdown 解析器 ├── prompt_builder.py # 系统提示词组装 ├── personality_state.py # 动态情绪状态机 ├── memory_store.py # 轻量记忆存储 └── watcher.py # SOUL.md 热加载模块职责单一各干各的。解析器只负责把文本变成结构化数据组装器只负责把数据变成最终的系统提示词状态机只负责维护动态情绪。这样设计的好处是你想换人格文件格式、想换情绪识别算法、想换记忆存储后端都可以独立替换互不影响。我见过不少项目把这几件事全塞进一个文件最后改一行情绪逻辑都要牵动整块 prompt 组装代码维护成本很高。4.2 解析器把 Markdown 变成配置SOUL.md 本质上是有约定的 Markdown解析并不困难但要注意处理各种边界情况。我的实现基于 dataclass核心代码如下# soul_parser.py import re from dataclasses import dataclass, field from typing import Dict, List dataclass class SoulConfig: name: str 爪爪 sections: Dict[str, List[str]] field(default_factorydict) emotions: Dict[str, str] field(default_factorydict) def parse_soul_md(text: str) - SoulConfig: config SoulConfig() current_section in_code False for raw_line in text.splitlines(): line raw_line.rstrip() # 跳过代码块内容防止误解析 if line.strip().startswith(): in_code not in_code continue if in_code: continue # 二级标题作为分节 if line.startswith(## ): current_section line[3:].strip() config.sections.setdefault(current_section, []) continue # 一级标题、空行跳过 if not line.strip() or line.startswith(#): continue # 列表项 if line.lstrip().startswith(- ): item line.lstrip()[2:].strip() if current_section: config.sections[current_section].append(item) continue # 键值对例如“高兴允许使用 1 到 2 个感叹号” if current_section 情绪表达规范 and re.search(r[:], line): key, _, val re.split(r[:], line, maxsplit1) config.emotions[key.strip()] val.strip() # 从身份认知里提取代号用于人格锚点 for item in config.sections.get(身份认知, []): m re.search(r代号[「\]?([^」\])[」\]?, item) if m: config.name m.group(1) break return config这个解析器有几个有意的处理。第一代码块内容不参与解析防止哪天在 SOUL.md 里放示例代码导致误读。第二所有区块统一存成 sections 字典情绪表达规范额外抽出一个键值对结构方便动态指令直接用。第三名字从“代号”两个字后面提取用正则宽松匹配不依赖固定格式。解析器的设计原则是容错优先——SOUL.md 是给人编辑的文本难免有格式不整齐的时候解析器宁可漏解析一条也不能因为格式问题整个崩掉。4.3 系统提示词组装管线解析完 SOUL.md接下来就是把人格指令组装成最终的系统提示词。这一步的关键是顺序和预算。我在组装器里写死了区块顺序人格锚点放最前面动态情绪指令放中间记忆放最后# prompt_builder.py from typing import List def build_system_prompt(soul, memory: List[str], state) - str: parts [] # 第一段身份锚点必须放在最前面 parts.append(f你是 {soul.name}。请严格按照 SOUL.md 中定义的人格回答问题。) # 按固定顺序组装各区块 section_order [身份认知, 性格特质, 价值观, 行为准则, 响应风格] for sec in section_order: items soul.sections.get(sec, []) if not items: continue lines [f【{sec}】] lines.extend(f- {item} for item in items) parts.append(\n.join(lines)) # 动态情绪指令由 PersonalityState 实时生成 emotion_text state.build_instruction() if emotion_text: parts.append(f【当前状态】\n{emotion_text}) # 记忆注入放在人格之后避免覆盖人格指令 if memory: mem_section [【短期记忆】, 以下是最近几条相关对话记忆用于保持连贯性] mem_section.extend(f- {m} for m in memory[-5:]) parts.append(\n.join(mem_section)) return \n\n.join(parts)有些人会觉得开头那句“请严格按照 SOUL.md 中定义的人格回答问题”是多余的但实测下来这句锚定作用很大。模型在长上下文中容易出现注意力漂移给它一句高优先级的指向性指令相当于在开始就立了一个“人格不能丢”的 flag。我还用一个轻量函数估算 token 开销避免系统提示词把上下文窗口占满def estimate_tokens(text: str) - int: 粗略估算中文约 1.5 字符/token英文约 4 字符/token。 cjk_chars sum(1 for c in text if \u4e00 c \u9fff) other_chars len(text) - cjk_chars return int(cjk_chars / 1.5) int(other_chars / 4)一个典型的 SOUL.md 模板组装出来的系统提示词大概在 550 到 1000 token 之间。这个量级在主流模型的上下文窗口里完全可接受但如果你在人格文件里堆了大量内容就要小心了。我给自己的硬性约束是系统提示词加记忆注入总 token 不超过模型窗口的 20%剩下 80% 留给对话本身。超出就砍记忆条数或者精简人格区块。4.4 动态人格状态机动态层是整个系统里最有“活着”感觉的部分。我用一个极简状态机管理情绪状态靠关键词触发切换连续中性交互后自动衰减回 neutral# personality_state.py class PersonalityState: 管理 AI 的短期情绪状态并在对话中动态调节表达风格。 NEGATIVE_WORDS [烦, 失望, 难过, 生气, 崩溃, 焦虑, 累] POSITIVE_WORDS [开心, 太好了, 谢谢, 厉害, 漂亮, 完美, 棒] def __init__(self): self.emotion neutral self.streak 0 # 连续中性交互计数 def update(self, user_text: str) - str: 根据用户文本更新当前情绪状态。 if any(w in user_text for w in self.NEGATIVE_WORDS): self.emotion comfort self.streak 0 elif any(w in user_text for w in self.POSITIVE_WORDS): self.emotion cheerful self.streak 0 else: self.streak 1 if self.streak 3: self.emotion neutral return self.emotion def build_instruction(self) - str: instructions { neutral: , comfort: 用户此刻可能情绪不佳。请先表达理解和共情再给出解决方案。语气要温和不要开玩笑。, cheerful: 用户情绪积极。你可以适当放松语气用更轻松的节奏回应但仍要保持内容质量。, } return instructions.get(self.emotion, )这个实现用了基于关键词的启发式识别不引入额外模型调用。实际效果对大多数场景足够而且零延迟零成本。如果你后续想更精准可以把关键词替换成一个小型情绪分类模型状态机的接口不用改。让状态机返回“指令文本”而不是只返回“状态名”这是我调试中养成的习惯直接生成可注入 prompt 的文本组装器拿着就能用不需要再维护一张状态到文本的映射表。记忆存储那部分我做了个轻量实现用固定长度的 deque 防止无限制增长再加一个简陋的“关键词相关性”方法供记忆筛选# memory_store.py from collections import deque import json class MemoryStore: 轻量记忆存储固定长度超长自动丢弃最旧记录。 def __init__(self, maxlen20): self.short_term deque(maxlenmaxlen) self._load() def add(self, role: str, content: str): self.short_term.append({role: role, content: content}) self._save() def relevant(self, query: str, top_k5): 极简相关性统计字符命中数实际项目可以换成向量检索。 keywords set(query) scored [] for item in self.short_term: score sum(1 for ch in item[content] if ch in keywords) scored.append((score, item)) scored.sort(keylambda x: x[0], reverseTrue) return [m for _, m in scored[:top_k]]相关性的实现很粗糙但对本地助手场景够用了。你甚至可以不做相关性直接把最近五条记忆全塞进 prompt效果差不了太多。真正要避免的是把几十条记忆全塞进去那会严重挤压上下文窗口。最后是热加载机制我写了一个简单轮询每两秒检查一次文件修改时间改了就用新的人格配置# watcher.py from pathlib import Path import time def watch_soul_file(path: Path, on_change): last_mtime path.stat().st_mtime while True: time.sleep(2) try: mtime path.stat().st_mtime if mtime ! last_mtime: last_mtime mtime on_change(parse_soul_md(path.read_text(encodingutf-8))) except FileNotFoundError: pass整套系统跑起来后的调用流程是新消息进来先更新情绪状态再组装系统提示词然后带着记忆调用模型接口最后把对话写入记忆。整个人格层对上层业务完全透明你在聊天平台接消息的时候根本感知不到人格层的存在但在用户眼里这个助手忽然就“有性格”了。5. 踩坑实录与排查技巧5.1 人格被“冲淡”了怎么办这是我遇到最多的问题同一份 SOUL.md前 20 轮对话人格表现非常鲜明越往后越像默认助手。排查下来有两个主要原因。第一个是对话轮次变长后人格指令在注意力上被边缘化模型开始以近期对话风格为主。第二个是用户消息里出现的措辞会反向影响模型比如用户一直很机械地提问AI 也会慢慢变得机械。解决方案是“人格锚点重置”。我在每 10 轮对话或者每次上下文窗口滚动后往对话消息序列里插入一条隐藏的提醒消息内容是“记住你是爪爪遵循 SOUL.md 的人格设定”。这条提醒不用太长一两句话就够但它能有效把模型拉回人格轨道。你也可以把同样的提醒放进系统提示词末尾相当于在 prompt 的首尾各立一个锚。实测下来双锚点的人格保持能力比单锚点高出不少。5.2 上下文窗口被长记忆挤爆人格系统的记忆如果放任不管很快会把上下文窗口塞满。我第一次跑长对话时就遇到了这个问题助手聊到 40 多轮开始丢上文再过几轮直接报错。后来我把记忆策略改成“短记忆保最近、长记忆做摘要”短期记忆只保留最近 10 到 20 条超过的旧对话在后台跑一次摘要把核心事实抽出来存成长期记忆。摘要不需要每次对话都做隔一段时间做一次就行。有个更省事的技巧给记忆条目加上时间衰减权重太久远的记忆在相关性筛选时权重打折。我试过在相关性的分数上乘一个 0.9 的幂次衰减效果不错。但在本地开源项目里你要优先考虑简单方案先用固定长度加最近 N 条撑不住了再上摘要。不要一上来就上向量数据库那是给自己找活儿。5.3 多平台语气不一致OpenClaw 接了好几个聊天平台我发现在不同平台上同一个 SOUL.md 的表现效果差异很大。有的平台会把 system prompt 严格放在开头有的平台却当成普通消息处理。这就导致同一套人格指令在 A 平台表现正常在 B 平台完全不起作用。我在平台适配层统一做了一件事不管平台怎么处理系统提示词我都在每条用户消息之前注入同一段人格锚点确保 AI 每次生成前都能“看到”人格指令。这段锚点很短成本低但有效保证了跨平台一致性。另外一个技巧是尽量把人格指令写得“像自然语言规则”而不是“像系统配置”因为某些平台会倾向于忽略格式化程度高的 content。5.4 问题速查表现象可能原因解决方案人格不稳定风格漂移指令埋太深或锚点不足在 prompt 首尾放双锚点定期插入提醒消息系统提示词挤爆上下文SOUL.md 太长或记忆注入过多精简人格区块记忆只保留最近 5-10 条模型不服从情绪指令指令写得太抽象改成可量化的描述比如“感叹号不超过 2 个”多平台表现不一致平台对 system prompt 处理方式不同在每条消息前注入统一人格锚点情绪状态切换过频关键词识别太灵敏增加 streak 计数要求连续多次触发才切换改完 SOUL.md 不生效没有热加载或缓存用 watcher 轮询文件修改时间自动重载配置调人格系统是个持续迭代的过程不可能一蹴而就。我在写第一版 SOUL.md 时也踩了不少坑比如写太多抽象的性格描述导致模型表现忽冷忽热。后来改成“特质加行为解释”的格式效果立刻稳了。再比如情绪关键词列表一开始写得太少用户说“好烦”和“烦死了”触发不了切换后来我把常见口语变体都收进去触发率才正常。这些细节指望一次想全是做不到的一定要跑起来看数据再反推修改。最后分享一个心得人格系统本质上是给 AI 建立“行为准则”而不是“思想”。别试图在 SOUL.md 里定义 AI 应该持有什么立场、信仰什么理念那是过度设计。你只需要定义行为边界和表达风格剩下的让模型在边界内自由发挥。实测下来边界清晰且内容精简的人格文件比试图面面俱到的人格文件稳定得多。这个平衡点建议你自己拿一个真实场景反复调调到自己觉得“这个助手像个靠谱同事”为止。