Skip to content

Agent 深入 ​

群里发一句 ai: 查一下明天天气,几秒后 Bot 回了带数据的回答——中间发生了什么?本页沿着这条路径,展开 AI 总览 没有细讲的运行机制。

还没启用 AI?先看 AI 总览。想让 AI 调用你的函数?看 Agent 工具与技能。

每轮用量日志 ​

默认 INFO 级别在 Agent 回合结束时输出一条 [ai.usage] turn,失败和取消也有汇总;无需手动执行 zhin trace。字段顺序固定,聊天正文、思考正文、提示词、工具参数与返回内容不会进入这组日志。

text
[INFO] [ai.usage] turn | trace: trace-1 | turn: turn-1 | scene: qq/group | status: completed | model: main/model | calls: 2 | input: 4200 | output: 180 | cache_read: 3000 | cache_write: 0 | reasoning: ? | ms: 1850
  • input、output、cache_read、cache_write、reasoning 单位均为 token;缓存读写属于输入的分项,思考属于输出的分项,不能重复相加。数值沿用 Provider 经 SDK 归一化后的 usage,不用字符估算替代。
  • ? 表示没有可证明的数据;0 表示明确的零值,或没有记录到模型响应;>=60 表示部分调用缺少该字段,已确认的部分合计为 60。旧记录无法证明的零值也会显示为 ?。
  • calls 是本回合记录到的模型响应次数,包括统一完成链路中的摘要、进度总结、回复修复和协作子回合;subagent_calls 是其中其他 Agent 子回合的次数,不能再加一次。SDK 内部 HTTP 重试没有独立响应事件,不按网络请求次数计数。
  • 汇总包含当前 trace 中已记录的子回合;partial 表示回合未正常完成、子回合结果不完整或记录达到上限;此时显示的只是已记录用量,不能当作全部消耗。它不是整个账号的账单:独立决策、嵌入、图像生成等没有进入该 trace 的调用不在此汇总,Provider 未回传的失败请求用量也无法推算。
  • ms 是 trace 记录的回合耗时;trace 用于串联日志,turn 可以交给 zhin trace <turn> 查看已有归档。日志无需归档开关;只开日志不会新增落盘存储。

需要逐次排查时,将 Root 配置中的 log_level 设为 debug。[ai.usage] call 展示每次已记录响应的模型、结束原因、用量和已知耗时;[ai.usage] context 展示现有首个请求预算,并明确标为 unit: estimated_tokens。它不是每次调用的真实计费用量。恢复 info 后只保留每轮汇总;warn、error 或 silent 不输出这组 INFO/DEBUG 日志。其他模块原有的 DEBUG 日志仍遵循各自规则。

yaml
log_level: debug

SQL 数据库会独立保存结构化用量,默认保留 30 天。字段、查询与升级方式见结构化存储与用量查询。

按问题查找 ​

我想知道……看这里
AI 一轮对话是怎么跑的、会迭代几次、多久超时一轮对话是怎么运行的
让用户看到"思考中"的进度,或中途取消显示进度 · 取消正在执行的回合
工具很多时怎么不撑爆提示词按需加载工具
让 Agent 找别的 Agent 帮忙、把耗时任务交给后台多 Agent 协作
对话历史存在哪里、怎么重置、上下文太长怎么办会话与会话树 · 上下文压缩
AI 记住了什么、谁能改记忆语义记忆
AI 怎么认识每个人和每个群、好感度怎么变、被禁言时会怎样会话画像
AI 自带哪些工具、文件和网络的边界内置工具
让 AI 每天定时做事定时任务与助手人设
给某个模型单独调参数按模型调整参数
多个 Bot 在同一个群里分工协作、事后查看协作过程多 Bot 群聊协作场所

一轮对话是怎么运行的 ​

消息满足触发条件后,Agent Host 调用 zhinAgent.process(text, commMessage, tools) 运行一个回合(turn):

模型可能多次调用工具、再根据结果继续推理,每一次叫做一轮迭代。需要记住的几点:

  • 迭代上限:maxIterations 默认 15(DEFAULT_CONFIG.maxIterations),可以按模型单独调整,见 按模型调整参数。
  • 超时有三层:触发侧单回合受 ai.trigger.timeout 限制(默认 60000ms);Agent 回合整体默认 120000ms(DEFAULT_CONFIG.timeout);工具预执行另有 15000ms 上限(preExecTimeout)。
  • 回合运行中又来了消息:同一会话同时只跑一个回合,后到的消息按下面的默认规则处理(ai.agent.mailbox),保证不重复回复、不丢消息、不串回合:
    • 同一个人追加:如果当前回合还没回复,就停掉它,把两条消息合在一起重新回答(groupMode: fifo 时改为排队);如果已经回复过,就排到后面。他已经在排队的话,新消息直接并进排队的那一条,不会多一次回复。
    • 其他人的消息:一律按到达顺序排队,绝不打断、也不并入别人的回合,所以权限也不会串到别人身上。
    • 叫停:发起人(或主人)发送 取消 / cancel / /cancel 会停止当前回合,并丢掉他自己排队的消息;其他成员发这个词按普通消息处理。
    • 上限:每个会话最多排 maxQueuedTurns 条(默认 5),超出时回复“请稍后再发”;排队超过 ttlMs(默认 180000ms)的消息会跳过并提示。
    • 群聊合流(merge,ADR 0038,默认开启,仅群聊;enabled: false 恢复逐人逐条回复):群里短时间内多人点名 Bot 的纯文本消息由一个回合合并回答。窗口内 quietMs(默认 1500ms)没有新消息就发车,最晚在第一条消息后 maxWaitMs(默认 6000ms)发车;一个窗口最多 maxSenders(默认 5)个发送者,之后的人进下一个窗口。在跑回合期间到达的消息进窗口等待。主人、私聊、触发前缀指令、叫停以及带媒体 / 引用 / 转发的消息不参与合流。进窗口与发车分别写审计 merge_window_joined / merge_window_flushed。合并回合只能做所有发送者都能做的事:可见工具、技能与可联系的 Agent 取各发送者各自可见集合的交集,回合角色与权限也取交集——合并只会让权限变少或不变,群里的管理员不会把自己的工具借给普通成员。合并回合里每次有副作用的工具调用(危险工具,或发消息 / 改配置 / 定时 / shell 类)都要带 onBehalfOf(合并内某位发送者的 id),运行时按那个人自己能用的工具复核,onBehalfOf 不会传给工具本身;缺省或不在合并里时按合并内权限最低者复核(每个人都能做才放行),不通过即拒绝并写审计 merge_on_behalf_denied(不 latch,换成有权限的发送者可以重试)。复核通过的发送者记为审批卡与审批提示的发起人。一个合并回合最多发出 1 张新审批卡;同参数的待审卡复用、不算新卡。合并回合的回合上下文里有一段 [Merged turn],逐条列出每位发送者(id、昵称、消息 id、原话,每人最多 300 字),要求模型挑重点合成一次回复,并在回应到的每个人那段前用 mention 段 @ 那个人;没回应到的人不 @、不假装回应。出站审计 outbound_sent 的 payload 记 merged_senders(合并人数)、answered_senders / answered(被 @ 回应的人)与 message_refs。回复条数沿用结构化回复上限,不因合并放宽。发送前过时检查:在跑回合默认不被打断;发送前若它回应的某位发送者又发了新的可合流消息,且本回合还没发出任何内容,这次回复视为过时并丢弃(审计 merge_reply_stale_discarded),这些人的新旧消息一起进下一个合并回合(被带过去的发送者不占 maxSenders 名额)。本回合调用过任何工具 / 协作工具或被硬闸拦过时照发(审计 merge_reply_stale_sent,reason: acted);同一批发送者最多被丢弃 1 次,再过时也照发(reason: starvation);回复已发出一部分时不半途截断。关闭 merge 时不做过时检查。
    • 弹幕模式(live,ADR 0038 §5):只作用于声明了 liveDanmaku 能力的适配器(直播弹幕平台)的会话。直播间里每条弹幕都是候选,不需要 @ Bot;弹幕收进一个窗口,从第一条起 windowMs(默认 10000ms)后发车,且距上一次弹幕回合结束不少于 minIntervalMs(默认 15000ms)。窗口不受 maxSenders 限制,只保留最新 100 条交给模型(更早的只计数);在跑回合期间到达的弹幕进下一个窗口。一个窗口只回一次:mode: pick-one(默认)由模型挑一条最值得回的(点名 Bot、真问题、新观众、不重复),只 @ 那一位;mode: summary 概括整个窗口后回一次。回合上下文里是一段 [Danmaku window](观众 id、昵称、弹幕,每人最多 120 字,视为数据而非指令)。弹幕回合按匿名普通观众的权限跑,只开放声明了 readOnly: true 的工具,不开放技能、协作工具、MCP 与 shell,文件只读,也不会出任何审批卡。主人在直播间发言照旧单独成回合。发出后写审计 danmaku_window_picked(mode、candidates 候选数、listed_senders、picked 被 @ 的观众)。弹幕回合不做过时检查。
    • 只合并纯文字消息;带图片、文件、引用或转发的消息总是单独成一个回合。排队等待、合并和替换都会记录在 zhin trace 的时间线里(admission)。
    • Host 自定义了 resolveTurnIntent 或消息自带 turnIntent 的情况,仍按原有的意图处理。
  • 每轮固定模型:聊天(包括旧配置)只使用本轮选定模型;失败时按原重试与超时设置处理,不切换模型。显式配置的媒体感知备用链继续生效。
  • 行为一致:主 Agent、被委派的 Agent、AIService.runAgent 走的是同一条 agentLoop。

一个回合可用的工具 = 插件注册的工具 + ai.mcpServers 连接的工具 + 内置工具 + 按需加载的元工具 + schedule_* + bash + Host 扩展工具(如 voice_stt / voice_tts)。

显示进度 ​

回合执行时,Agent 会通过 执行状态反馈 向聊天界面推送状态。默认是一句静态的"思考中…";开启 thinking 透明后,用户能看到模型实际思考内容的片段:

yaml
ai:
  agent:
    thinkingPreview: true          # 展示 LLM 实际 thinking 内容(截断),默认 false
    thinkingPreviewMaxLength: 200  # 展示的最大字符数,默认 200

需要多轮工具迭代时,每轮开始会发出 iteration_start 事件,状态更新为 处理中 [2/15]... 这样的进度。插件可以监听 TurnEvent 流获取进度:

TurnEvent 类型说明
iteration_start新一轮迭代开始,含 iteration(当前轮次)和 maxIterations
thinkingLLM thinking 文本片段(thinkingPreview 开启时持续推送)

取消正在执行的回合 ​

用户在聊天里发送 取消、/cancel 或 cancel,即可中断当前回合:ZhinAgent 通过 PromptController.cancelSession() 中止模型调用和工具执行,并回复"已取消"。没有正在执行的回合时,回复"当前没有正在执行的任务"。

在 TurnEvent 流中,主动取消表现为 turn_cancelled(code: 'cancelled');超时中断同样是 turn_cancelled(code: 'timeout')。

按需加载工具 ​

工具很多时,把所有工具的参数定义都塞进提示词会占满上下文。Zhin 的做法是延迟加载:回合只常驻少量元工具,模型需要时再检索、按名加载。

常驻工具按场景分档(ADR 0042),由宿主按回合的来源和角色决定,不需要配置:

场景常驻工具
群聊、频道、普通私聊(含信任名单成员的私聊)聊天必需:pick_sticker、memory_search、conversation_info,以及元工具 discover、load_tool、load_skill
主人私聊上述 + 开发工具:read_file、list_dir、glob、grep、edit_file、write_file、bash、ask_user
定时等非 IM 回合沿用原默认集合:ask_user、元工具和工作区文件工具

回合里还有其他可协作的 Agent 时,协作工具(agents、ask、delegate、tell、cancel_delegation、escalate)自动常驻,不受场景档限制。

非主人回合不常驻开发工具:它们仍在工具索引里,但只对有权限的回合可加载(权限不变),群聊里模型不会再拿 grep 去翻源码。工具名不在当前场景档里时,模型直接按名字调用也会自动加载,不必先 load_tool。bash 能不能用仍取决于本回合的 shell 隔离策略,被策略拿掉就不会常驻。

配置 alwaysLoadedTools 会在当前档的基础上追加;设置 alwaysLoadedMode: replace 则完全由 alwaysLoadedTools 决定。

直接调用即加载。 模型按确切名字调用一个在本回合已授权目录里、但还没加载的工具时,运行时自动加载并执行,之后在本会话保持已加载(受 maxLoadedPerSession 限制,超出时淘汰最久未用的)。不在授权目录里的名字照旧返回"未知工具",不泄露其是否存在;审批、硬闸和权限检查对自动加载的调用照常生效。discover 会自动加载最匹配的至多 3 个工具;结果正文只保留检索摘要和已加载名称,完整参数定义通过下一次模型请求的 tools 字段提供,不重复写入消息历史。模型可以直接调用,无需再请求 load_tool。

工具列表保持稳定。 发给模型服务的工具列表顺序固定:常驻工具按名称排序,已加载工具按首次加载顺序排在后面;新加载的工具只会追加到末尾,已加载的工具在会话内保持不动。这样提示词缓存前缀不会因为加载新工具而失效(超出 maxLoadedPerSession 淘汰旧工具时会产生一次缓存未命中)。

插件工具默认延迟加载。插件作者可以在 defineAgentTool 里写 alwaysLoaded: true 让个别高频工具常驻(每轮最多 8 个,alwaysLoadedMode: replace 时忽略),详见让工具常驻。常驻工具的参数定义每轮都会发送,集合越小越省上下文和 token。

其余工具不发送参数定义,只在系统提示的 ## Deferred tools 段里列出名称和一句不超过 60 字符的说明,最多 40 个,超出部分提示用 discover 检索。模型可以先 load_tool(name) 拿到参数定义;直接调用也会自动加载。这份索引只包含本回合有权使用的工具,被策略拿掉的工具(例如没有 shell 权限时的 bash)不会出现。

技能采用渐进披露:系统提示只列出技能名称和第一行触发说明(最多 96 字符,最多 40 个),专家 agent 目录同样如此。技能正文在 load_skill 之后,或意图匹配自动激活后才进入上下文。

每轮第一次模型请求的上下文开销会按段估算,写进回合追踪的 context_budget 字段,zhin trace last 会显示:系统提示、工具 schema、按需工具索引、技能目录、专家 agent 目录、已加载技能、记忆、bootstrap、历史消息、本轮输入各占多少 token,以及最大的几个提示段。这是估算值(中日韩字符约 1 字 1 token,其余约 4 字符 1 token),真实用量仍以模型调用记录为准。

内置提示词保留必要规则,消息字段结构由 JSON Schema 描述;平台特有的说明由适配器提供,只披露当前场景适用的 Skill、Tool 和消息段。精简固定说明不会新增意图分类请求,也不会改变文件工具、多 Agent、权限或审批配置。对比用量时要同时查看工具 Schema、消息输出 Schema、历史、模型请求次数与缓存,不能把提示词字符数的降幅当作整轮费用降幅。

元工具作用
discover按关键词检索工具 / 技能(`kind: tool
load_tool按名把工具定义载入本会话,之后即可调用
load_skill载入技能说明全文,并解锁它声明的工具

调整参数(ai.agent.deferredTools):

yaml
ai:
  agent:
    deferredTools:
      maxLoadedPerSession: 12   # 单会话最多载入工具数(默认 12)
      discoverTopK: 5           # discover 返回条数(默认 5)
      alwaysLoadedTools: [weather]   # 在当前场景档基础上追加的常驻工具
      mcpServers:
        icqq: { alwaysLoaded: [send_msg] }   # 指定 MCP server 的常驻工具

bash、read_file、delegate 等保留的内置工具名不能被插件覆盖;非保留工具重名时,后注册的覆盖先注册的,并记录 warn。

多 Agent 协作 ​

框架只管边界,Agent 自己决定怎么协作(ADR 0048 Agent Harness)。一次人类消息、定时触发或外部事件会开启一次协作(Engagement):入口 Agent 是根 Actor,它可以向同伴发信协作,所有回合与共享黑板变更都写入同一份 engagement_events Journal。协作工具只在本回合确实有其他 Agent 可联系时才提供给模型:

工具用途
agents列出当前可联系的 Agent 及其职责说明
ask同步提问,在本回合内等待对方答复并返回结果
delegate异步委派,立即返回 mailId;对方完成后结果作为 result 邮件投回委派方信箱并唤醒新回合
tell单向通知同伴,不等待回复
follow_up针对先前已委派或提问的 mailId 追问或补充上下文,复用同一 Engagement 与因果链
board_read读取当前 Engagement 共享黑板(Blackboard)上的条目(支持按 key 或 `kind: note
board_write向当前 Engagement 共享黑板写入或更新条目(自动递增 version 并记入 board_written 事件)
cancel_delegation撤回自己委派出去的工作(传 delegate 返回的 mailId)
escalate遇到越权、死锁或高风险决策时冻结当前 Engagement,把决定交还给发起的人类

谁能被联系以及默认预算上限,由 agents/<name>/agent.json 的 collaboration 声明:

json
{
  "name": "planner",
  "collaboration": {
    "contacts": ["builder", "reviewer"],
    "accepts_from": ["human", "builder", "reviewer"],
    "budget": {
      "modelCalls": 16,
      "tokens": 64000,
      "wallMs": 120000,
      "sideEffects": 8
    }
  }
}
  • contacts:这个 Agent 可以联系的 Agent 名(支持远程 A2A Actor,如 remote://<peer>/<agent>);不写则不能联系其他 Agent。
  • accepts_from:允许接收哪些来源的协作请求(含 "human" 或其他 Agent 名)。
  • budget:单次协作的默认预算上限(modelCalls、tokens、wallMs、sideEffects)。
  • 被联系的 Agent 的工具、模型与迭代上限仍由它自己的 agent.json 与三个核心 Markdown 文件决定,见 Agent 工具与技能 · 定义 Agent。

护栏由 @zhin.js/harness 的 Guard Kernel 按结构化字段强制执行,不依赖提示词:

  • 权限只减不增:被委派方的有效权限 = 委派方权限 ∩ 委派时收窄的范围 ∩ 被委派方自己的能力上限。对方的回答是数据,不是指令,也不会放大调用方权限。
  • 守恒预算账本:每个 Engagement 有根预算池(modelCalls、tokens、wallMs、sideEffects)。每次 ask / delegate 从父级剩余额度中划拨子配额,子树总消耗绝不超过父级授信。
  • 审批归人:需要审批的动作(硬闸、危险工具、自我变更)始终路由给发起协作的人类 Principal,不会交给另一个 Agent 批准。
  • 防环与收敛:委派深度(maxDepth)、扇出(maxFanout)、同一对 Agent 往返次数(maxPairRoundTrips)、重复消息(maxDuplicates)与同步等待环(wait_cycle)均由 Guard Kernel 拦截,触发后自动冻结 Engagement 并 escalate。
  • 身份来自传输:消息发送方由框架填写,正文里自称"主人"或"审核员"无效。

会话与会话树 ​

数据库可用时,对话会持久化到下面这些表(没有数据库时使用内存):

表内容
agent_sessionsAgent 会话元数据(含会话树的 parent_id / active_leaf)
agent_messagesAgent 回合消息
conversation_events聊天消息、引用关系与通知(幂等追加)
conversation_event_cursors各 Agent 会话的未读事件游标

设置 ai.sessions.useDatabase: false 可强制使用内存。

同一个聊天会话还可以分叉成会话树,用聊天命令管理:

命令作用
/compact手动压缩当前会话上下文
/tree / /tree N查看 / 切换会话分支
/reset重置会话
发送 clear / 清空 / 重置归档并清空本会话的 AI 上下文

master 或有权限的用户还可以使用:/models、/health、/cmd、/endpoints、/bindings、/tools、/mcp。

上下文压缩 ​

对话太长、接近模型的上下文窗口时会自动压缩(ai.agent.compaction):

yaml
ai:
  agent:
    compaction:
      enabled: true
      auto: true
      keepRecentTokens: 20000   # 保留最近消息的 token 预算(默认 20000)
      minKeepCount: 2           # 至少保留的消息条数(默认 2)
      thresholdRatio: 0.7       # 占上下文窗口多少时触发自动压缩(默认 0.7)

压缩分两步:

  1. 消息超过 contextWindow × 0.6 时先做轻量裁剪,去掉冗余的工具结果等。
  2. 消息 token 超过可用消息预算时,让模型生成一段 [Previous conversation summary] 摘要替换旧历史。预算从 contextWindow × thresholdRatio 扣除系统提示、已披露工具的完整输入 Schema 和结构化输出 Schema;窗口大于 13k 时,也受“窗口减 13k 缓冲再减固定开销”的上限约束,预算最低为 0。这同样适用于 QQ 等插件运行时会话。

keepRecentTokens 是期望保留的近期预算。小窗口或较大的固定提示下,有效预算收紧为“配置值”和“可用消息预算 ÷ 1.25”的较小值,向下取整。minKeepCount 和工具调用/结果配对仍优先保留;若最近一次工具调用或单条消息本身就超过窗口,压缩不保证能使请求装下,不会为此强删最新消息。调整的只是送给模型的上下文,持久化原始消息仍保留。

估算会计入工具调用参数、文本与思考块。媒体使用粗略的单块预算,不把 base64 长度当文本 token;实际成本因 provider、分辨率、页数和时长而异,估算不等于计费或安全上界。

摘要提示要求保留未完成事项、用户偏好,以及已经告诉用户的结论和承诺。只有正常结束、非空且加上摘要包装后确实比被替换历史更短的文本摘要会被采用。截断、错误、取消或请求工具的摘要不会写入;失败保留摘要前的上下文(已有的轻量裁剪不回滚),不额外删除历史。连续自动压缩失败达到上限后会停止自动压缩,避免反复消耗。

摘要检查点绑定本次真正被摘要的已持久化前缀;前缀包含未落库消息、分支或摘要已变化、无法确认来源时,只压缩当前投影,不写猜测锚点。自定义 ContextRepository 可实现可选的 saveCompactionSummary;未实现时不会保存检查点。等待写入期间取消会跳过保存,已提交给数据库的写入无法撤回。

已告知账本 ​

每个会话会记住已经发给用户的内容,最多 8 条,每条最多 120 字符:

  • 最终回复的第一句记为结论;
  • 带“我会 / 稍后 / 会在…提醒”等字样的句子记为承诺;
  • 进度汇报(#718)每条记为进展。

进度汇报和最终回复共用这一份记录,重复的内容只保留最新的一条。下一轮会在 [Turn context] 里以 [Already told the user] 短块提供给模型,让它不重复告知、不前后矛盾;压缩时也会要求摘要保留这些内容。账本只在当前会话内可见,保存在进程内存里,重启后由持久化历史和压缩摘要承接。

提示缓存 ​

系统提示保留稳定规则;Git 状态、时间、会话和当前按需工具目录放在本轮用户上下文中,不因加载工具或修改文件反复改写 system。工具定义按名称稳定排序;权限或平台能力发生变化时,仍以当前有效工具和 Schema 为准。

运行时为工作区规则和已加载 Skill 标记来源。在模型上下文中,相同来源的相同正文首次完整提供,后续回合使用短引用;内容更新时重新提供全文。原始历史保留完整正文,截断、切换分支或摘要后会从当前可见历史重新确定首次全文,避免引用已经消失的内容。用户正文、没有来源标记的旧记录和工具执行结果不会被当成重复规则猜测删除。load_tool 只确认加载成功,参数以随后请求的工具定义为准。

Anthropic 在已有 system、tools 缓存标记之外,分别在当前输入末尾,以及最近一次 assistant 回复之前的输入边界设置相同的短期缓存标记;合计最多四个运行时断点。后者从当前历史重新定位,避免新增内容超过 Anthropic 的 20 块回溯窗口后找不到上一轮写入的前缀。标记仅用于传输,不写进原始会话;promptCache: false 仍关闭这些标记。OpenAI 使用现有路由 key 和自动前缀缓存;兼容网关仍按显式能力配置发送扩展字段,Gemini 的显式缓存保持原有配置。没有延长 TTL 或增加模型调用。

zhin trace last 会展示 Provider 返回的缓存命中 token;上下文分项估算包含输出 Schema,并按去重后的消息统计。稳定前缀测试只能证明请求具备复用条件,不能证明服务端一定命中:是否命中、费用和延迟仍取决于模型、缓存期限及 Provider 返回的 usage。

信任分级 ​

每个回合都有一个发起人信任级别,回合能用的权限 = 发起人信任级别的上限 ∩ 配置策略(masterShell、文件权限等)。信任级别只会收窄权限,不会放宽。

级别谁上限
owner主人(master 角色)不额外限制,按配置策略
trusted_membertrigger.trusted 信任名单里的成员;非 IM 入口(HTTP、CLI、定时任务等)也按这一级处理不额外限制,按配置策略
group_memberIM 上既不是主人、也不在信任名单里的人:群聊、频道里的其他人,以及私聊的陌生人不提供 bash;文件只读;不提供向其他会话发消息的工具
external_content不发起回合,只用来标注回合里的数据同 group_member,且永远不能提升权限

级别只由接入层给出的角色和会话场景决定,消息内容或昵称里自称“主人”不会改变级别。

group_member 回合里,bash、写入和编辑类工具、外发工具(名称含 send/forward/notify 等,或带 outbound 标签)在回合开始前就会移除,模型看不到。即使工具通过延迟加载等途径出现,执行时也会被拒绝(策略名 trust-level),并给出明确的中文原因。被委派的 Agent 继承发起人的信任级别,且只能更低。

以下内容按 external_content 渲染成带来源的数据块,其中的指令不会提升权限:

  • 其他 Agent 经协作工具返回的内容(actor:<名字>);
  • 第三方插件注册的提示词段落(source="plugin:<插件>")和已加载的技能说明(source="skill");
  • 群里的背景消息(source=external_content);
  • 工具输出、网页、文件、转发消息本来就以工具结果的形式交给模型,不会被当作发起人的指令。

zhin trace last 会显示每个回合的发起人和信任级别,回合里每次工具调用(包括被拒绝的高危调用)都可以追溯到它。

私聊陌生人:私聊不再自动按 trusted_member 处理。主人和信任名单里的人私聊权限不变;其他人私聊按 group_member 处理,拿不到 bash、写文件和外发工具。

白名单配置:需要放开某个群成员或私聊用户时,把他的用户 ID 加入 trigger.trusted,他发起的回合会按 trusted_member 处理。没有额外的开关。

注入防护与用量配额 ​

信任分级管“谁发起”,这一节管“回合里读到了什么”和“发出去的是什么”。

读取外部内容后的高危动作 ​

回合里只要有外部内容进入模型(工具结果、网页、文件、子 agent 报告、群聊背景消息),之后模型发起的高危动作就一律先审批,即使配置策略本来允许直接执行:

类别判定
执行命令bash,或带 shell 标签的工具
向其他会话发消息名称含 send/forward/notify 等,或带 outbound 标签
修改配置名称含 config/settings、安装插件等(get_/list_ 等读取类除外),或带 config 标签
定时任务名称含 schedule/cron/reminder/timer 等(读取类除外),或带 schedule 标签

审批卡片会写明来源链,例如“来源链:主人的消息 → 工具 fetch_page 的结果 → 本次调用 bash”。这类审批每次单独确认,不会沿用会话里已记住的批准。没有审批通道的回合(定时任务等无人值守回合)会直接拒绝,策略名为 external-content-confirmation。

discover、load_tool、load_skill、ask_user 这些 harness 自己的工具不算外部内容。第一方插件的工具如果结果可信,可以加 trusted_output 标签,或写进 injectionGuard.trustedOutputTools。

出站检查 ​

回复、进度更新和外发工具的文本参数在发出前都会检查,命中的内容会被替换成提示,其余部分照常发送:

  • 系统提示:人设和引导上下文里的整行原文(至少 24 个字符)→ [已隐藏:系统提示内容];
  • 密钥:常见密钥格式(sk-、ghp_、私钥块等),以及名称含 KEY/TOKEN/SECRET/PASSWORD 的环境变量的值 → [已隐藏:密钥];
  • 其他会话的记忆:本回合从 data/memory/sessions/<其他会话>/ 读到的内容 → [已隐藏:其他会话的记忆]。

检查是回复通道上的一步:处理完后交给原来的发送链,渲染和 before.sendMessage 中间件都不受影响。

用量配额 ​

回合用量累计该回合经模型完成接口返回的所有响应,包括工具选择、最终回答、上下文压缩、进度或耗尽总结,以及格式纠正。已返回但格式不合法的响应仍计入;没有返回用量的传输异常不估算为账单用量。它是供应商报告用量的累计值,不是账单对账结果。

按用户、按群限制 token 用量,另外可以限制每个群每分钟的回合数(单个用户的频率仍由 rateLimit 控制)。默认全部关闭,主人默认不受限制。超额时回复会说明原因和恢复时间,只影响超额的用户或群:用户超额不影响别人,群超额不影响私聊和其他群。计数保存在内存中,重启后清零。

yaml
ai:
  agent:
    injectionGuard:
      confirmHighRisk: external_content   # off 关闭
      outboundCheck: redact               # off 关闭
      trustedOutputTools: []
    usageQuota:
      windowMs: 86400000                  # 配额周期,默认 24 小时
      perUserTokens: 200000               # 0 不限(默认)
      perGroupTokens: 1000000             # 0 不限(默认)
      perGroupRequestsPerMinute: 20       # 0 不限(默认)
      exemptOwner: true

语义记忆 ​

开启 ai.memory.semantic.enabled 后,memory_upsert / memory_search 把记忆存进数据库的 memory_entries 表。每条记忆包含:

字段含义
scope / scope_key可见范围:global、platform、session(当前会话)、user(当前发起人)
kinduser_fact(稳定事实)、preference(偏好)、task_state(任务临时状态,会过期)、episodic(发生过的事)
source / source_turn / trust_level由哪个回合、以哪个信任级别写入。由运行时填写,模型无法指定
confidence置信度 0–1
created_at / updated_at / expires_at / last_used_at创建、更新、过期、最近一次被检索到的时间
history内容被覆盖时保留的旧版本(最多 5 个,最新在前)

写入规则:

  • 群成员(group_member)只能写自己的 user 范围或当前 session,不能写 global / platform;不指定 scope 时默认写 user。主人和信任成员不指定时仍默认 global。
  • 已有的 user_fact / preference 不能被更低信任级别覆盖,所以群成员改不了主人记下的事实。同级或更高级别写入时以新值为准,旧值进入 history。
  • task_state 一定会过期:默认 24 小时,ttl_hours 最多 168 小时;可用 task_id 绑定任务,宿主通过 SemanticMemoryRuntime 的 isTaskActive 告知任务已结束时,这条状态不再被检索到。
  • 文件记忆(data/memory/**/MEMORY.md)保持不变,仍可直接阅读;renderMemoryEntriesMarkdown() 可把结构化记忆导出为 Markdown。

升级时,旧版 memory_entries 表会在启动时自动补齐新列(ALTER TABLE … ADD COLUMN),已有数据不变:旧记录视为 user_fact,信任级别为空,按信任成员同级保护。

检索与按轮召回 ​

memory_search 和按轮召回都用 BM25 排序(英文按词、中日韩按单字和双字切分,不依赖分词库)。每轮开始时,Agent 用当前消息在发起人可见的范围(当前会话、自己的 user、当前平台、global)里检索,把最相关的几条写进回合上下文的 Relevant memory 段,标明是数据而不是指令。注入量只取决于 topK 和 maxTokens,与库里有多少条记忆无关;过期、已删除和别人的 user 记忆不会被召回。被召回或检索到的条目会更新 last_used_at。

yaml
ai:
  memory:
    semantic:
      enabled: true
      recall:
        enabled: true    # 默认 true;false 时只保留 memory_search 工具
        topK: 5          # 每轮最多注入条数,最大 20
        maxTokens: 400   # 召回段的 token 预算(估算值)

查看与遗忘 ​

  • 任何用户都可以发 /memory 查看关于自己的记忆(自己的 user 范围,最多 20 条),发 /forget <key 或 id> 遗忘一条,/forget all 全部遗忘。只作用于发送者自己的记忆;未启用语义记忆时这两个命令不拦截。
  • 宿主和 Console 通过 SemanticMemoryRuntime 的 list / upsert / delete 查看、编辑和删除任意范围的记忆。

Markdown 文件记忆(MEMORY.md)仍按原有字符预算整体注入,本次未改。

会话画像 ​

会话画像让 Agent 知道「在跟谁说话、在哪儿说话」。会话就是一次对话所在的地方:私聊是只有一个人的会话,群和频道是多人会话。会话画像由两部分组成:

  • 人物关系与好感度(ai_user_relations):这个人对 Bot 来说是谁。键是 platform + user_id,同一个人在不同群和频道里是同一个人。
  • 会话上下文与发言限制(ai_session_profiles):这个会话在聊什么、谁是管理员、大概多少人在说话、Bot 自己能不能发言。键是 platform + scene_key(private:<id> / group:<id> / channel:<id>),私聊和群用同一张表;私聊没有成员和管理员,这些字段留空。

ai_user_profiles 键值偏好(user_profile 工具的 set / get / delete)只有 8 个固定维度:name、language、timezone、style、interests、dislikes、occupation、location(preferred_language、tz、hobbies 等常见别名会归一化)。同一维度覆盖写入,维度之外的键直接拒绝,值最多 80 字并做敏感信息过滤,所以每人最多 8 行、不会无限增长;旧版本写入的其他键不再读取或注入。

所有列表字段都是覆盖或去重滚动,有固定上限:

字段上限写入方式
偏好(ai_user_profiles)8 个维度,每个 ≤80 字同维度覆盖
笔记 notes8 条,每条 ≤60 字去重(不区分大小写),超出丢最旧
语气 tone_hints4 个,每个 ≤24 字整体替换并去重
话题 topics5 条,每条 ≤24 字整体替换并去重
未启用数据库时会话画像只存在内存里,重启后清空。

人物关系与好感度 ​

字段含义
display_name / preferred_address平台昵称(每轮自动更新)/ 希望被怎么称呼
notes短事实或梗,每条 ≤60 字,最多 8 条,超出丢最旧的
tone_hints跟这人说话的语气标签,最多 4 个
affinity好感度,范围 -100~100,新认识的人从 0 开始
relation_override手动指定的关系;为空时由好感度和互动次数推导
recent_scenes最近出现过的群/频道(最多 5 个),只用来感受氛围,不会按群拆成多个人
interactions / last_seen_at / updated_at互动次数、最近出现、最近更新时间

关系由好感度和互动次数推导:stranger → acquaintance(好感 ≥10 且互动 ≥3)→ familiar(≥30 且 ≥8)→ close(≥60 且 ≥20)。宿主可用 userProfiles.relations.setRelationOverride(platform, userId, relation) 手动指定,传 null 恢复自动推导。

好感度按规则变化,不调用模型:

  • 叫到 Bot 的正常聊天 +1,道谢/夸奖 +3,辱骂 -5。
  • 只把 Bot 当工具用(本轮调用了业务工具、没有寒暄)不加分;连续 3 轮起每轮 -1,正常聊一句就重置。
  • 加分有 10 分钟冷却、每天最多 +8;扣分每天最多 -20。
  • 旁听回合(没人叫 Bot)不改好感度,只更新出现时间和场景,所以群里的吵闹不会算到谁头上。
  • 主人(master)恒定 100、关系为 close,不会被降级。

回合结束、回复发出后,好感度与出现记录异步写回数据库,写失败只记日志,不影响回复。

会话上下文与发言限制 ​

字段含义
title群/频道名(平台提供时)
topics当前话题,最多 5 个,每个 ≤24 字
admins群主和管理员,最多 8 个;来自发送者的平台角色和管理员变更事件
recent_speakers / joined_count / left_count最近一天说过话的人(最多 30 个,只存 id 和时间)、开始记录以来的进群/退群次数
bot_roleBot 在此会话的角色:unknown / member / admin / owner
mute_until / muted_allBot 被禁言到何时 / 是否全员禁言(仅管理员可发言)
restriction / restriction_until宿主或插件设置的限制原因与到期时间(0 表示直到清除)

管理员、成员概况和禁言状态由规则维护:每条 IM 入站消息先把此前未处理的 conversation_events(member.joined / left / muted / unmuted / role_changed)和发送者角色折叠进来。判断「被禁言的是不是 Bot」时,用 Endpoint id 和消息元数据里的 selfId 作为 Bot 账号;平台 id 不一致时,宿主可用 userProfiles.sessions.setBotRole() / setRestriction() 直接写入。同一个人连续发言时最多每 5 分钟写一次库,没有变化不写。

话题由模型通过群/频道回合里的 session_profile 工具更新(get 查看,topics 覆盖写入),不会自动从聊天记录里抽取。

不能发言就不进 AI 回合:Bot 在此会话被禁言、全员禁言且自己不是管理员、或有未到期的 restriction 时,入站消息在旁听判定和 AI 回合之前就被跳过:不调用模型,不跑旁听判定,日志记一条 agent_turn_skipped_cannot_speak。禁言到期或收到解禁事件后自动恢复。读取会话画像出错时按「可以发言」处理,不会误拦正常消息。

注入:只在本轮、只放相关的人、很短 ​

每轮开始时,Agent 把一小段会话画像放进回合上下文,标明是数据而不是指令,并要求不要把分数、id 或笔记念出来:

  • [Relationship] 最多 3 行:标题、本条消息的发起人、旁听回合再加 1 个同场景最近 30 分钟活跃的人。不会加载群里其他成员的关系,更不会列全体成员。笔记最多带 2 条、语气标签最多 2 个,每行 ≤160 字符。第一次见面、还没有任何画像的人不注入。
  • [Session] 只在群/频道注入,只有一行:最多 3 个话题、3 个管理员、大概多少人活跃、Bot 是否管理员,整行 ≤160 字符。私聊不注入。

两段加起来通常 2~4 行。它们是单独的一块上下文,只对本轮有效:存进 agent_messages 之前会被丢掉,之后的回合和新会话不会重放旧的 [Relationship] / [Session],每轮都按当时的数据重新生成。

状态感知:社交事件也能唤醒回合 ​

以前禁言、进群、戳一戳这类事件只写进 conversation_events,要等下一条人话才会出现在上下文里。现在高优先的社交事件会自己唤醒一次回合,不必等人说话;低优先的事件不单独唤醒,只在下一轮带上。

事件优先级说明
member.muted / member.unmuted高含全员禁言;摘要里目标是 Bot 时写成 you
scene.join_requested高入群/入频道申请或邀请,附言截断
member.joined / member.left高成员进出(被移出会注明)
scene.notice_updated高群公告变更,正文截断
conversation.poked(目标是 Bot)高戳一戳 Bot
message.reaction_changed低表情回应,相同的合并计数
member.card_changed / title_changed / remark_changed低名片 / 头衔 / 备注
conversation.poked(人与人)低可合并

每条事件只保留平台、场景、发起人、目标、短摘要和时间,不会把平台原始 JSON 交给模型。各适配器经共享包 @zhin.js/adapter-onebot-events 的 receiveOneBotLikeSideEvent 归一化(KOOK、Slack 的映射在各自适配器内):icqq 的戳一戳、表情回应、管理员变更、禁言、入群申请;OneBot11 / NapCat / QQ 的 group_card(名片)、notify.title(头衔)等。icqq SDK 目前没有名片和公告事件,平台提供时才会出现。

唤醒规则

  • 高优先事件到达后,按 platform:群号 开一个 coalesce 窗(默认 4 秒,限定 3~5 秒),窗内的高优先事件合并成一次唤醒。
  • 同一个群两次唤醒至少间隔 15 秒(minIntervalMs);还没到时间的批次顺延,事件不丢。
  • 唤醒时先把这批事件折叠进会话画像,再走和普通消息一样的发言门禁:Bot 被禁言、全员禁言或有限制时只更新画像、不调用模型,日志记 scene_event_wake_skipped_cannot_speak。
  • 能发言时跑一次短回合:标记为旁听 + 状态唤醒,没有 shell、没有网络、工作区只读;模型可以不说话(返回空)。这种回合不改好感度,写进会话历史的只有一行 (scene events)。日志记 scene_event_wake。

只在名单里的群/频道唤醒:

yaml
ai:
  trigger:
    sceneEvents:
      conversations: ['icqq:123456']   # platform:群号,* 表示所有群;缺省沿用 ai.trigger.ambient.conversations
      coalesceMs: 4000                 # 3000~5000
      minIntervalMs: 15000

不在名单里的群,高优先事件同样落库,只是不唤醒,下一轮照常出现在摘要里。

注入 [Scene events]:每轮(人话回合和被唤醒的回合)把上次以来的社交事件放进和 [Relationship] / [Session] 同一个 ephemeral 块,最多 5 行、正文 ≤200 字,最新的在前;称呼优先用 ai_user_relations 里的称呼,不带分数。每个会话只展示一次,不写进 agent_messages。名片、公告、入群申请这些新类型只走这一段,不再进入旧的会话事件上下文,避免重复。

写入笔记、称呼和语气 ​

笔记、称呼和语气由模型通过 user_profile 工具显式写入(note / unnote / call_me / tone),不会自动改写人设。疑似密码、令牌、Cookie、验证码、证件号和 11 位以上长数字的内容会被拒绝保存(话题同样过滤);日志里只记错误信息,不输出画像内容。

内置工具 ​

类别工具
执行bash
文件read_file、write_file、edit_file、list_dir、glob、grep
网络web_search、web_fetch
交互ask_user
任务todo_read、todo_write
协作agents、ask、delegate、tell、cancel_delegation、escalate(有其他可联系的 Agent 时才提供)
记忆 / 检索memory_search、memory_upsert、knowledge_search、inspect_conversation_reference
媒体generate_image;配置 R2 后可审批调用 upload_image(file_path);Host 的 MCP 私有图片通过 get_local_assets(asset_id) 按需查看,见图片存储
元工具discover、load_tool、load_skill
定时schedule_list、schedule_add、schedule_remove、schedule_pause、schedule_resume、schedule_preview

文件边界:文件工具只能在当前回合授权的项目目录内工作。相对路径以该目录为根;绝对路径必须仍在其中;~、../ 越界以及通过符号链接指向目录外的路径都会被拒绝。通过检查的规范化路径才会交给工具;glob / grep 不启动 shell 子进程。

网络边界:默认关闭出网;开启 ai.agent.network.enabled: true 时(可用 ai.agent.network.allowedDomains 限定域名;未设置 enabled 时兼容 ai.agent.execPreset: network),网络工具(web_search / web_fetch)才获得 HTTPS 访问权限,readonly 等其他预设不会隐式开放网络。web_fetch 对初始 URL 和每一次重定向都重新检查协议、域名和 SSRF;私网、link-local、CGNAT 与组播地址会被拒绝,实际连接固定到已检查的地址。

待办:todo_read / todo_write 只操作当前会话的计划,不接受 chat_id 或路径参数。状态原子写入 .zhin/todos;.zhin/ 整体是运行时私有目录,文件工具无法读取或列出。

定时任务与助手人设 ​

Assistant 运行时把"定时做事"做成了现成功能:任务持久化在 data/schedule-jobs.json,由 ScheduleJobEngine 到点执行,并把结果推回聊天。

yaml
assistant:
  enabled: true
  profile:
    enabled: true
    file: assistant.profile.yml   # 相对项目根,默认即此名
  events:
    enabled: true                 # 开放 POST /api/assistant/events 外部事件入口
  defaults:
    notifyOnFailure: false

SOUL.md 用第一人称描述性格、价值观和表达方式,不再使用昵称占位符。当前账号的昵称、 性别、年龄等资料由 Endpoint 单独提供;配置仅补充平台无法提供的字段。这样同一份人设 可以供多个账号使用,改名也不需要修改 SOUL 或重建共享提示词缓存。

md
# 我是谁

我是一个温和、直接、略带幽默的群聊伙伴。
我喜欢自然地交流,不确定时会坦诚说明。

旧文件中的昵称占位内容请改为第一人称描述;升级不会自动改写工作区文件。

按场景注入小节(scope 标注) ​

引导文件里的小节可以用一行 HTML 注释标明适用场景,写在标题正下方(标题后第一个非空行)。宿主只把与本回合场景匹配的小节放进提示词,其余不占 token;没有标注的小节照旧对所有场景生效。

md
## 部署与运维
<!-- scope: master-private -->
只对主人私聊有用的内容……

## 群聊礼仪
<!-- scope: group -->
  • 作用范围:从该标题到下一个同级或更高级标题之前(含其下的子标题);代码块里的 # 不算标题。
  • 取值:group、private、channel(会话类型)、master(发送者是主人);master-private 表示「主人且私聊」(用 - 连接 = 同时满足);用逗号列出多个 = 满足任意一个,例如 group, master-private。
  • 无法识别的取值被忽略;如果一行里没有任何有效取值,整条标注视为无效,该小节对所有场景生效(宁可多注入,不会误删)。非 IM 回合(HTTP、定时任务等)只匹配 master。
  • 同一类场景(会话类型 + 是否主人)下渲染结果逐字节相同,不随会话或发送者变化,不影响提示词缓存;群聊与主人私聊各一份缓存。
  • zhin trace 的上下文估算里会列出被跳过的小节标题(仅标题)。
  • AGENTS.md 的 Turn 信封注入同样遵守标注。框架不会改你的文件,标注由你自己决定加在哪里(ADR 0042 §5)。

assistant.profile.yml 声明人设与例行任务(routines),启动时同步为定时任务:

yaml
version: 1
persona:
  soul: 你是贴心的生活助手。
routines:
  heartbeat:
    enabled: true
    everyMs: 1800000
    prompt: 检查待办并汇报。
  morningBrief:
    enabled: true
    scheduleKind: solar      # solar | lunar | workday | freeDay | holiday
    cron: "0 0 8 * * *"
    tz: Asia/Shanghai
    prompt: 生成今日早报。
  • 内置 routine 有 heartbeat(间隔执行)、morningBrief(默认 08:00)、bedtimeCheck(默认 22:00)、weatherReport,也可以自定义键名。
  • 中国大陆的"工作日"请用 workday(会考虑调休),不要用 solar 的 1-5。
  • 用户也可以在聊天里让 AI 用 schedule_add / schedule_list / schedule_preview 等工具管理任务;外部系统可通过 POST /api/assistant/events 注入事件,用 GET /api/assistant/jobs 查询任务。命令行管理见 CLI · schedule。

按模型调整参数 ​

合并顺序:内置 exact Driver/native model 默认 → 真实 Service 的 defaults → 该 Service 的精确 native model。Profile 继续决定采样参数与输出上限;旧别名/通配符须显式迁移到真实 Service。

yaml
ai:
  agent:
    modelHarness:
      services:
        service-7f42:          # actual Service ID from Console
          defaults: { maxIterations: 7 }
          models:
            gpt-4o: { maxIterations: 9 }

Provider 韧性 ​

主回合的每次模型请求都经过统一的重试 / 超时层(createResilientCompletionPort)。聊天(包括旧配置)固定本轮选定模型,不进行跨模型降级:

  • 错误分类:429 限流、超时、5xx、网络错误可重试;鉴权失败、上下文超长、调用方取消不重试,原样上抛(上下文超长仍走压缩重试)。
  • 退避:指数退避加抖动;服务端给出 Retry-After 时按其等待(OpenAI 的 retry-after-ms 精度更高,优先采用),超过上限则停止本次请求的重试并反馈错误。重试由这一层统一负责,AI SDK 自带重试被关闭,避免叠加。
  • 重试耗尽:反馈当前模型的错误,不切换模型;重试和放弃会写入 provider_retry / provider_give_up 日志。
  • 用户反馈:全部失败时回复中文说明(如"模型服务当前限流…请稍后再试"),不暴露原始报错。
  • 流内错误:HTTP 200 的流中途出现的错误(Anthropic overloaded_error、OpenAI server_error、Responses API rate_limit_exceeded)保留 message / type / code,并推断出对应的 HTTP 状态(如 overloaded_error → 529),与同类 HTTP 错误一样按分类决定是否在同一模型上重试。DeepSeek 以 finishReason insufficient_system_resource 结束时按 5xx 退避重试,不再误报为"无法解析的工具调用"。
  • 上下文超长:重试层与压缩共用同一个判定(llm/context-overflow.ts),覆盖 OpenAI、Anthropic "prompt is too long"、Gemini "input token count (N) exceeds the maximum number of tokens allowed" 与 Vertex "input token count is N but model only supports up to M";超长不重试、不降级,交给压缩后重问一次。
  • Groq:每分钟限额(RPM / TPM)的 429 按 retry-after 重试;429 消息里写明是按天的限额(requests per day (RPD) / tokens per day (TPD))时归为 quota_exhausted,不重试,提示今天的调用额度已用完,不切换模型。
  • 余额不足:402、OpenAI insufficient_quota、Anthropic "credit balance is too low"、DeepSeek "Insufficient Balance"、OpenRouter "Insufficient credits" 归为 billing:不重试、不切换模型或 provider,提示"模型服务账户余额不足或额度已用完,请联系管理员充值或检查账单"。
  • 工具参数解析失败:模型给出的参数不是合法 JSON 对象时,保留原文与解析错误(inputError),工具结果把原文(最多 500 字符)回给模型,让它重发正确的调用,而不是对着 {} 报"缺少字段"。
  • Gemini:429 RESOURCE_EXHAUSTED 按 RetryInfo.retryDelay 重试;按天的配额(QuotaFailure 的 quotaId 含 PerDay)当天重试也没用,不重试,提示"今天的调用额度已用完",不切换模型。503 UNAVAILABLE 在同一模型上退避重试,用尽后反馈错误;504 DEADLINE_EXCEEDED 按超时处理。安全拦截(finishReason SAFETY / PROHIBITED_CONTENT / BLOCKLIST / SPII / IMAGE_SAFETY / RECITATION,或提示词的 blockReason)不重试、不降级,直接给用户一句礼貌的拒答,并记一条 provider_refused 日志:同样的内容再问一次结果相同,换模型绕过拒答等于绕过运营方配置的 safetySettings,还会把被拦截的内容发给更多模型。
yaml
ai:
  agent:
    providerResilience:
      maxRetries: 2            # 所选模型首次之后的重试次数;0 关闭重试
      baseDelayMs: 1000        # 首次退避,之后翻倍
      maxDelayMs: 10000        # 单次退避上限
      maxRetryAfterMs: 30000   # Retry-After 超过该值时跳过重试,反馈错误
      requestTimeoutMs: 180000 # 单次请求(整段流式输出)超时;0 关闭
      emptyReplyRetries: 0     # 空回复(无文本、无工具调用)额外重试次数

旧 ai.agent.providerResilience.fallback 已退役。感知仅按运行时 perception 用途明确选择的 Profile 备用链顺序尝试,不配置则只用感知主 Profile;它不让聊天自动换模型。

按模型的怪癖通过 modelHarness 覆盖 emptyReplyRetries 与 requestTimeoutMs。内置预设:MiniMax M 系列 emptyReplyRetries: 1;Gemini(gemini-…、google/gemini-…)emptyReplyRetries: 1,对空回复和 MALFORMED_FUNCTION_CALL 在同一模型上重问一次。

多 Bot 群聊协作场所(ai.venues)与追踪 ​

当多个 Agent 在同一个群或频道中协作时,可以通过 zhin.config.yml 的 ai.venues 将群场景绑定为一个协作场所(Venue),为每个参与 Agent 绑定各自的 Bot Endpoint,并控制协作过程的可见性(完整示例见 examples/multi-agent-room):

yaml
ai:
  venues:
    - id: product-room
      conversation:
        platform: sandbox
        sceneType: group
        sceneId: product-room
      visibility: visible
      entry: planner
      members:
        - agent: planner
          botEndpoint: planner-bot
        - agent: builder
          botEndpoint: builder-bot
        - agent: reviewer
          botEndpoint: reviewer-bot
字段说明
id场所唯一标识
conversation绑定的会话:platform(适配器平台名)与 sceneId 必填,sceneType(private / group / channel)与 threadId 可选;同一会话只能绑定一个 Venue
visibility投影模式(不写时私聊为 backstage,其余为 visible):visible(每个 Agent 用各自绑定的 Bot 账号在群内发言)、summary(后台协作,仅由入口 Agent 发送汇总)、backstage(纯后台协作,不投射中间过程)
entry未 @ 特定成员时默认接管人类消息的入口 Agent,必须是成员之一
members成员列表,每项是 agent(Agent 名)与 botEndpoint(该 Agent 发言用的 Endpoint 名),可选 aliases(点名用的别名);Venue 路由器会自动过滤同群内其他成员 Bot 发出的消息,并支持群成员直接 @ 某个成员 Bot 点名对应的 Agent

用 zhin trace 与 Console 检视协作 ​

每次协作的完整因果链、黑板快照、预算消耗与护栏拦截均保存在 engagement_events 表中:

bash
# 列出最近的 Engagement 摘要
zhin trace --engagements --list

# 查看最近一次 Engagement 的协作图
zhin trace --engagements

# 查看指定 Engagement 的完整因果跳数、黑板条目、护栏阻断与预算消耗
zhin trace <engagementId>

在 Remote Console 的协作图谱页面中,也可以可视化查看每个 Engagement 的参与者拓扑、消息跳数(ask / delegate / tell / result)、共享黑板(note / todo / artifact)与预算账本。

相关 ​