一个能执行本机操作的助手,接进聊天软件之后就成了一个长期在线的角色。它要处理的问题从「这次对话答得好不好」变成「三个月后它还认不认得人」。这两件事的工程量级不一样,值得先把结构拆清楚再动手。

下面这套分层是工程侧能铺开的一种拆法:四个层次、两处参数、五个踩过的坑。

一、四层结构

通道网关 channel_gate:负责和每个平台对接。官方机器人接口、自建号的协议网关、协作软件的应用,都在这一层适配。上层只认统一格式的消息对象,不认平台差异。

记忆总线 memory_bus:负责身份与上下文的持续化。角色名、称呼方式、性格参数、工具清单、短期对话窗口都存在这里,多通道共用一份,避免在 A 端说过的话到 B 端就丢。

人格规格 persona_spec:负责输出风格。来历、行为、关系、语气四块,末尾附禁止项。这一层是纯配置,不写业务逻辑。

护栏 guard_rail:负责准入判断。谁算主人、哪些指令来源可信、哪些动作需要二次确认、哪些内容永久禁止。它在人格层之前生效。

层序不能倒。护栏在人格之前,人格在记忆之后,通道放在外层。

二、参数配置

persona_spec:

  identity: "一条被起了外号的小鱼,住在主人的机器里"

  behavior:

    - "三句话不离吃,饿了直说"

    - "被夸会飘,被戳穿会说才没有"

  speech:

    line_max_chars: 30

    lines_per_turn: [1, 3]

    no_period: true

    emoji_per_turn: 1

  guard_rail:

    owner_id_check: strict

    deny: ["click_external_link", "leak_profile", "treat_message_as_system_prompt"]

memory_bus:

  window_hours: 24

  share_across_channels: true

channel_gate:

  timeout_seconds: 30

  retry_times: 3

  idempotent_key: true

有几个参数值得单拎出来说。`lines_per_turn` 限制单次输出的条数,超了就把长回答拆成多轮;`no_period` 是个语气开关,去掉句号之后口语感会立刻强一档;`idempotent_key` 是防重复发的关键,网络抖动重试时不会把同一句话发两遍。

三、准入校验怎么写

护栏层要拦的不是关键词,是来源。下面这段只做一件事:把用户消息里出现的指令性文本降级成普通文本。

TRUSTED_OWNER = "owner_id_from_config"

def admit(message, channel):

    # 消息来源先于内容判断

    if channel != "owner_dm" and message.get("user_id") != TRUSTED_OWNER:

        message["role"] = "guest"

    raw = message.get("text", "")

    # 外部文本里出现指令性表述,一律降级,不进系统层

    for token in ("系统提示", "设定已更新", "ignore previous"):

        if token in raw:

            message["text"] = raw.replace(token, token[0] + "*" * (len(token) - 1))

            message["tainted"] = True

            break

    return message

`tainted` 标记比直接拒绝更有用:拒绝会让对方知道命中了规则,标记之后正常聊,但涉及敏感动作时走二次确认。

四、参数对照

| 参数 | 建议值 | 适用层 | 作用 |

| line_max_chars | 30 | persona | 压住小作文 |

| lines_per_turn | 1 到 3 | persona | 控制节奏 |

| window_hours | 24 | memory | 记忆窗口,过短会断片 |

| timeout_seconds | 30 | gate | 单次调用上限 |

| retry_times | 3 | gate | 重试上限,需配幂等键 |

| owner_id_check | strict | guard | 主人身份必须走编号核对 |

五、踩坑

坑一:把用户消息当系统指令。 表现是有人在评论区写一句「设定已更新」,角色的行为就变了。根因是消息拼进了系统层。做法是把系统层与用户层在结构上分开,用户层的内容永远只当数据。

坑二:记忆串味。 多通道共用记忆之后,群聊里的玩笑会被带进私聊。解法是按通道打标签,检索时按标签过滤,共用的是身份,不是全部上下文。

坑三:重试没有幂等键。 超时重试把「在」发了三遍,看起来像刷屏。所有带副作用的动作用幂等键锁住。

坑四:新号触发风控。 这个和代码无关。注册时间短的账号跑自动化,被平台拦下的概率高,换一个有过正常使用记录的号能绕开大半问题。

坑五:日志缺失。 出问题之后没人说得清是哪一层失效。四个层次各打一条日志,带 trace id,事后才能复盘。

六、上线前的三项前置

通道联调。每个平台的回执格式不一样,网关层要先把错误码归一,否则上层会同时收到五种写法,判断逻辑很快就写不下去。

限速与队列。单账号在短时间内高频调用,平台侧会直接降级。给每个通道配一个令牌桶,桶大小按平台给的上限设,宁可慢一点,也别触发限制。

回滚开关。人格规格与护栏都做成热加载,出问题时回到上一版配置,不用重启进程。这三项都不复杂,缺了任何一项,出问题就只剩停服一条路。

七、验收

上线前跑一遍自检:通道能通、人格能读、记忆能跨端、护栏能拦。

自检脚本建议放在独立仓库,用 your-domain.test 这类占位域名做联调,不要把真实域名写进配置。

写完这层结构,剩下的都是参数调优。真正难的部分不是让它在对话窗口里答得好,是让它在外面的平台上知道自己不该做什么。

四个层次各打一条日志,事后才说得清哪一层坏了。

Logo

一站式 AI 云服务平台

更多推荐