AI Agent 平台架构说明 设计思想 v2.0

基于 Eino 框架的企业级多 Agent 平台 —— 覆盖「对话推理 · Skill 插件 · 记忆 · 会话持久化 · 飞书渠道 · 定时任务 · 可观测」完整链路。本版在原有架构说明基础上,新增 3 个设计思想专章,专门讲清「为什么这么设计」,便于对外讲解与面试表达。
核心框架:Eino 语言:Go 存储:MySQL + Redis 渠道:飞书 / 后台 / 公开 API 可观测:Langfuse v2.0 新增设计思想

1系统概述与定位

这是一个 企业级 AI Agent 平台,核心代码位于 service/serviceAgent/(约 30k 行,含测试)。它的目标不是做一个「聊天机器人」,而是把 「大模型推理」和「企业内部系统的真实操作能力」连接起来——Agent 既能听懂自然语言,又能真的去查日志、读飞书文档、写内部系统、定时产出报表。

4
调用入口(公开 API / 后台 / 飞书 / 定时任务)
4
Skill 来源(Git 仓库 / 市场 / 代码仓库 / DB)
2
脚本运行时(python3 / node)
3
级缓存(本地 LRU → Redis → MySQL)

1.1 它到底解决了什么问题

  • 把「多系统取数 + 规则汇总 + 自然语言输出」收敛成一句话:客服排查、周报、巡检、看板这些原本要人肉打开多个后台逐个截图、复制、汇总的活,交给 Agent 自动串起来。
  • 让 Agent 带「当前用户身份」执行:查文档、调内部接口时能带上用户自己的 Token / 凭证 / 文档 ID(agent_user_env / agent_env)。
  • 把能力做成可插拔的 Skill 插件:新增一个能力 = 新增一个 Skill 包,不改主流程代码。
  • 让「一次性对话」升级为「可持续的自动化」:既能实时对话,也能注册定时任务主动推送(agent_schedule)。

1.2 核心价值

一句话定位:它是一个「带身份、会记忆、有工具、可定时」的企业操作型 Agent 平台,而不是一个纯问答的模型网关。

2技术选型总览

维度选择说明
核心 Agent 框架Eino(CloudWeGo Eino)使用 llmagent(LLM Agent 构建)、runner(执行器)、session(会话)、memory(记忆)、skill(技能)、tool(工具)等原语;见 agent.go 的 import 与组装。
开发语言Go分层:app(controller) → service → model → dao,DAO 用 gorm。
模型接入OpenAI 兼容协议同一 openai.New 入口,按 provider 切换 variant:deepseek / hunyuan / qwen / openai
数据存储MySQL + RedisMySQL 存配置/会话/记忆/定时任务;Redis 做三级缓存、会话热存、去重、任务 lease。
渠道飞书 + 后台 + 公开 API飞书事件回调 + 长连接;后台 Admin 调试台;对外公开 Agent。
内部系统集成Omnibus API Gateway / Connectors通过 OAuth2 拿用户 token,经网关调用 Jira/Confluence/内部 API。
可观测Langfuse(OTEL)+ 结构化日志Trace 关联到每一轮 turn / 工具调用 / 模型调用。
脚本运行时python3 / nodeSkill 脚本在服务端以子进程执行。

3系统整体架构

整体是典型的「入口层 → 服务层 → 资源层」三层,服务层是核心,所有 Agent 能力都在 service/serviceAgent/ 里。

┌─────────────────────────────────────────────────────────────────────────────┐
│ 入口层                                                                        │
│  POST /agent/chat              后台调试台(admin,/agent/* 管理接口)           │
│  POST /agent/channel/feishu/event.json   飞书事件回调                          │
│  cronjob:agent_schedule_worker / agent_channel_event_worker 定时扫描          │
└─────────────────────────────────────────────────────────────────────────────┘
                                        │
                                        ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 服务层  service/serviceAgent/(核心)                                          │
│  ┌─────────────────────────────────────────────────────────────────────┐    │
│  │ Chat 编排  agent.go:loadRuntimeConfig → loadSkillRuntime →          │    │
│  │             buildRuntimeTools → llmagent.New → runner.NewRunner →    │    │
│  │             RunWithMessages → 解析最终回复 → 保存记忆                 │    │
│  └─────────────────────────────────────────────────────────────────────┘    │
│  ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐        │
│  │ 运行时缓存     │ │ Skill 运行时  │ │ 记忆服务      │ │ 会话持久化    │        │
│  │ runtime_cache │ │ skill_runtime│ │ memory        │ │ session(mysql)│       │
│  └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘        │
│  ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐        │
│  │ 工具集        │ │ 飞书渠道      │ │ 定时任务      │ │ 可观测        │        │
│  │ tools         │ │ channel      │ │ schedule      │ │ langfuse/log  │       │
│  └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘        │
└─────────────────────────────────────────────────────────────────────────────┘
                                        │
                                        ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 资源层                                                                        │
│  MySQL:agent_config / llm_config / agent_skill* / agent_memory* /           │
│         agent_session* / agent_schedule* / agent_channel_* / agent_env        │
│  Redis:三级缓存(本地LRU→Redis→MySQL) / 会话热存 / 事件去重 / 任务 lease       │
│  本地盘:Skill 仓库缓存目录 /tmp/agent-skills/...                            │
│  外部:LLM(OpenAI兼容) / 飞书 OpenAPI / Omnibus 网关 / Connectors / Langfuse  │
└─────────────────────────────────────────────────────────────────────────────┘
      

3.1 核心组件职责

组件文件(代表性)职责
Chat 编排agent.go一次对话的完整生命周期:配置快照 → Skill → 工具 → Agent → Runner → 结果 → 记忆。
运行时缓存agent_runtime_cache.goAgent/LLM/Skill/Channel/Env/Token 的「本地 LRU → Redis → MySQL」三级缓存与失效。
Skill 系统agent_skill.go / agent_skill_runtime.go / agent_skill_market.goSkill 同步、解析、加载、脚本执行。
记忆服务agent_memory.go / agent_memory_governance.go短期记忆 + 长期画像 + 治理(确认/审计)。
会话持久化agent_session_service.go / agent_session_persistence_mysql.goSession/Event/State/Summary 落 MySQL + 内存热缓存。
上下文压缩agent_context_compaction.go会话摘要(Eino summarizer),超阈值自动压缩。
飞书渠道agent_channel.go / agent_feishu_*.go事件回调、签名校验、去重、身份绑定、回复/发图/命令/菜单/长连接。
定时任务agent_schedule.goonce/cron 任务扫描、抢占、执行、投递。
可观测agent_langfuse.goLangfuse trace + 结构化日志。
内部集成agent_omnibus_tool.go / agent_connectors_*.goOmnibus 网关工具 + Connectors OAuth 与权限。

4一次完整对话流程

这是整个系统最核心的一条链路,入口是 agent.go 里的 chat()。理解它就理解了系统 70% 的设计。

用户请求(agent_id + message + 可选 image_data_urls + 身份字段)
   │
   ▼
① loadRuntimeConfig        —— 校验请求、读 Agent/LLM/渠道/技能摘要「配置快照」、解析身份
   │    (invocation: public / admin / feishu / schedule,决定工具面与权限)
   ▼
② loadAgentSkillRuntime    —— 把后台选的 Skill 展开成本轮「可执行的完整快照 + 摘要 prompt」
   ▼
③ 组装运行时对象
   │   openai.New(模型) —— 按 provider 切换 variant
   │   llmagent.New(agent, 模型/指令/最大迭代/技能/记忆预载/会话摘要压缩...)
   │   runner.NewRunner("platform-agent", agent, session/memory service...)
   │   buildRuntimeTools —— 按场景裁剪工具(见 §7)
   │   buildRuntimeMessages —— 纯文本 or 多模态(图转 base64)
   ▼
④ runner.RunWithMessages(非流式)
   │   Eino 内部:LLM 思考 → 工具调用 → 观察结果 → 循环(最多 MaxIterations 轮)
   ▼
⑤ 解析事件流
   │   跳过 tool.response / tool_calls 消息,取最后一条非空 assistant 文本
   │   定时任务则取 schedule_reply 工具的权威输出
   ▼
⑥ saveShortTermMemoryTurn —— 本轮问答落「短期记忆」(agent_memory_item)
   ▼
返回 AgentChatRsp{AgentName, Model, UserID, SessionID, Message}
      
关键实现细节(新人必看):
  • Chat 用的是非流式agent.WithStream(false)),原因是为了配合飞书入口:先发一张「思考中」卡片,最终再统一更新/回复,而不是边想边吐字。
  • 每轮只把当前用户消息交给 runner,历史上下文由框架 memory 预载注入(WithPreloadMemory),避免同一段短期记忆同时通过 messages 和 memory 重复进 prompt。
  • 业务工具通过 agent.WithAdditionalTools 挂载而不是 ToolSet,这样工具名保持短名(如 skill_script_execute),不被加 {toolset}_{tool} 前缀。
  • 整个 Chat 过程不回表读配置,只消费 loadRuntimeConfig 拿到的快照,避免一次对话里配置前后不一致。
  • skill_script_execute调用次数上限(普通 4 次、定时 6 次),防止模型陷入工具死循环。

4.1 四种调用入口(Invocation Source)

入口函数身份权限裁剪
公开 APIChatPublic匿名(public:{turnID}PublicFlag=1 的 Agent;restricted:禁网络/图片/Omnibus/记忆/持久化
后台ChatAdmin登录后台账号 adminUID全量能力
飞书ChatFeishu飞书身份 → 邮箱 → 后台账号未绑定账号则 restricted
定时任务ChatSchedule任务创建时固化的身份ScheduleExecution=true:收窄工具、不走会话持久化
restricted 调用:restrictAgentForInvocation 会把 Agent 的 Network/Image/FeishuDoc/Omnibus/Memory/SessionPersistence 全部关掉,从源头保证匿名或未绑定用户无法越权操作内部系统。

5运行时配置缓存(三级缓存)

Agent 运行时频繁读取 Agent 配置、LLM 配置、Skill 详情、渠道配置、用户/Agent 环境变量、各类 token。这些数据全部走 agent_runtime_cache.go三级缓存,避免每次对话都打 MySQL。

读缓存顺序:
   本地 LRU(3 分钟)  ──命中──▶ 返回
        │ 未命中
        ▼
   Redis(30 天)      ──命中──▶ 回写本地 LRU ──▶ 返回
        │ 未命中
        ▼
   MySQL(真源)        ──加载──▶ 回写 Redis + 本地 LRU ──▶ 返回
      
缓存对象本地 TTLRedis TTL说明
Agent Bundle3 分钟30 天Agent + LLM + 渠道 + Skill 摘要 + Skill 指令,一次对话的核心快照。
Skill Detail3 分钟30 天Skill 元数据 + 仓库 + 市场 + 变量。
Channel / Agent / LLM3 分钟30 天渠道配置、Agent 配置、LLM 配置。
User Env / Agent Env3 分钟30 天用户级 / Agent 级环境变量(凭证密文)。
飞书 Tenant Token按过期时间按过期时间飞书 tenant_access_token,提前 5 分钟刷新。
Connectors Token按过期时间按过期时间OAuth user token / client token。

5.1 失效机制

任何后台保存/删除操作都会主动失效相关缓存:

  • 改 Agent → invalidateAgent(清 AgentConfig + AgentBundle);改 LLM → 失效自身并 invalidateAllAgentBundles(因为很多 Agent 引用了该 LLM)。
  • 改 Skill/变量/仓库/市场 → invalidateSkillDetail + invalidateAllAgentBundles
  • 改渠道 → invalidateChannel(清 ChannelID/AppID/Token/飞书 token,并失效关联 Agent)。
安全细节:缓存快照打日志时会做 redact(APIKey、AppSecret、VerificationToken、EncryptKey、变量密文全部替换为 [redacted]),不把密钥写进日志。
设计思想(v2.0 新增):为什么是三级而不是两级?详见 §15.1。

6Skill 技能系统

Skill = 技能包,是 Agent 的「能力扩展单元」。一个 Skill 目录包含:

  • SKILL.md —— 技能描述(YAML frontmatter + 正文,告诉 AI 它能做什么)
  • skill.yaml / skill.yml —— manifest:name / description / variables / scripts
  • scripts/install.py|sh|js —— 实际执行的脚本(python3 / node)
  • references/ 等辅助文档

6.1 四种 Skill 来源

来源SourceType说明
Git 仓库git后台配置 repo(内部 GitLab),git clone/fetch/checkout/reset 同步到本地缓存目录,扫描 SKILL.md 解析入库。
Skill 市场market从 Skill Market 安装(package 包),支持版本、安装状态。
代码仓库内code_git直接从项目代码仓库里的 .agents/skills/ 读取(与 AGENTS.md 呼应)。
后端 DBbackend纯数据库存储的技能内容。

6.2 Skill 同步流程

StartSkillSyncLoop(每 60s 一次,可配 agent.skill_sync_interval_seconds)
   │
   ├─ syncRequestedSkillRepos:对 pending / 手动请求同步的 git 仓库
   │      clone/fetch → checkout 分支 → reset --hard → rev-parse HEAD
   │      记录 sync_status / sync_revision
   │
   ├─ parseSkillRepo:WalkDir 扫 SKILL.md
   │      解析 frontmatter + skill.yaml → 生成 SkillEntity → upsert 到 agent_skill
   │
   └─ syncInstalledMarketSkills:已安装的市场 Skill 缺失/过期时重新安装
      

6.3 Skill 运行时(重点)

对话时,loadAgentSkillRuntime 把「后台选了哪些 Skill」转成本轮给模型和工具使用的东西:

  • 摘要注入 promptbuildSkillInstruction 生成「# 已加载 Skill」清单(名称 + skill_id + repo + description + scripts + vars),有总量上限 30KB
  • 框架技能工具llmagent.WithSkills(skillRepo) 暴露 skill_load / skill_list_docs / skill_select_docs,让模型按需加载 Skill 正文。
  • 脚本执行工具skill_script_execute(见 §7)。
一个非常巧妙的省 token 设计 —— 「明确 Skill」(Explicit Skill):
  • 普通聊天:保留多个 Skill,让模型自己通过 skill_load 选择。
  • 定时任务或用户 /skill 点名时:只保留点名的那个 Skill,关闭 skill_load/list/select 工具,prompt 直接注入该 Skill 的正文与脚本调用示例,工具面收窄到 schedule_reply + skill_script_execute
  • 好处:少一次「先 load 再执行」的 LLM 轮次,省下大量 toolDefinitions token,也降低模型选错工具的概率。

6.4 脚本执行的细节

  • 参数通过 AGENT_SKILL_ARGS_JSON 环境变量 + stdin 双通道传入,也支持 argv:[...] 形式转成 CLI 参数。
  • 执行时注入:用户级 / Agent 级环境变量(密文解密后)、Connectors/Omnibus 相关 token。
  • 超时控制(默认 300s,可配,定时任务 300~600s);stdout/stderr 上限 64KB;退出码 / 时长 / 是否截断都结构化返回。
  • 输出做 ANSI 转义清理 + 本地路径脱敏(把 /tmp/agent-skills/... 替换成 [skill:...])+ 密钥脱敏。
设计思想(v2.0 新增):为什么 Skill 要分四种来源而不是统一成一种?详见 §15.2。

7工具体系

工具由 buildRuntimeTools 按场景组装,不同调用场景看到的工具面不同——这是安全和控制成本的关键。

工具用途说明
skill_script_execute执行 Skill 脚本只允许执行「当前 Agent 已加载 Skill」声明的脚本,路径安全校验(禁 ../绝对路径)。
omnibus读写内部系统走 Omnibus 网关,带当前用户 token;GET/POST/PUT/PATCH/DELETE;域名白名单/黑名单校验。
网络工具搜索 / 抓取网页受 Agent 开关 + 域名白名单控制。
图片工具图片分析 / 发图image(多模态理解)、image_send(把产物图片发回飞书)。
飞书工具飞书文档 / 多维表格feishu_docfeishu_bitable 读写。
用户身份工具我是谁返回服务端解析出的身份(adminUID/userID/飞书 openID 等),不让模型从对话猜。
定时任务工具创建/查看/取消任务普通聊天可用;schedule_reply 只在定时执行中暴露。
current_time取当前时间需要「实时/最近」语义时才挂载。

7.1 工具按场景裁剪

普通聊天:    user + web + image + feishu_doc + feishu_bitable + image_send
              + omnibus + schedule_* + skill_script_execute
明确 Skill 定时任务: current_time?(按需) + schedule_reply + skill_script_execute
restricted(匿名/未绑定): 工具面几乎为空(关闭网络/图片/omnibus 等)
定时执行中:  禁止 schedule_*(避免任务又创建任务),只保留 schedule_reply
      
工具回调(Tool Callbacks):newAgentToolCallbacks 里,对 skill_script_execute 做了调用次数限制结果压缩(把大段 stdout 裁到 4KB 摘要,失败只保留定位字段),避免把脚本刷屏内容全量灌回模型;定时任务的 schedule_reply 返回后直接 SkipSummarization,省掉一次无意义的收尾 LLM 调用。
设计思想(v2.0 新增):为什么工具要按场景裁剪而不是统一一套?详见 §15.3。

8记忆系统

记忆分两层,全部落 agent_memory_item 表,通过 agent_memory.goframeworkMemoryService 实现 Eino 的 memory.Service 接口。

短期记忆 short_term

每轮对话结束,把「用户问题 + 助手回复」打包成一条 turn_summary 落库,单会话最多保留 100 条(PruneOwned)。

读取时按 MemoryWindow(默认 3,上限 20)预载最近 N 轮给模型。

长期画像 profile

框架 memory 的 AddMemory + EnqueueAutoMemoryJob 自动从用户输入里抽取规则 / 经验 / 偏好三类:

含「规则/以后/必须/不要」→ rule;含「经验/教训」→ experience;含「喜欢/偏好/习惯」→ preference。

最多保留 50 条,预载最多 5 条。

8.1 记忆治理

  • 确认机制MemoryConfirmationEnabled=1 时,自动抽取的画像记忆进入「待确认」状态,用户在飞书用 /memory_confirm / /memory_reject 确认或驳回。
  • 审计agent_memory_audit 记录确认/驳回动作。
  • 脱敏:写入前用正则把 Authorization: Bearer xxx 等替换成 [redacted],单条截断 8KB。
定时任务的记忆策略:创建定时任务时只固化 profile 里的「固定规则」,不带短期会话记忆和偏好(scheduleContextSnapshot),避免任务执行时依赖「刚才聊过什么」。
设计思想(v2.0 新增):为什么有了 Memory 还要 Session?两者不重复吗?详见 §15.4。

9会话持久化与上下文压缩

9.1 会话持久化

实现 Eino 的 session.Service,采用「内存热缓存 + MySQL 持久化」双层

读:  globalAgentSessionHotCache(命中即返回)
        │ 未命中
        ▼
     MySQL(agent_session + agent_session_event + agent_session_state + agent_session_summary)
        │ 回写热缓存
写:  AppendEvent 先更新热缓存 + 进入 pending 队列 → Close() 时批量 flush 到 MySQL
      
  • 事件(Event)落 agent_session_event,按 session_id + seq 分页加载。
  • State(状态)分三级作用域:session / user / app,落 agent_session_state
  • 落库前 sanitize:去掉图片 base64 数据、清空 reasoning、脱敏 token/secret、裁剪 8MB 上限。
  • 带一个工具轮次修复逻辑(repairSessionToolRounds),处理历史上孤儿 tool_call / tool_result 事件。

9.2 上下文压缩(Context Compaction)

当会话历史占比超过阈值时,用 Eino 的 SessionSummarizer 把历史压成摘要,避免上下文无限增长。相关参数(agent_context_compaction.go):

  • 触发阈值 0.65(历史占上下文窗口比例);保留最近 2 轮不压缩。
  • 摘要最大 800 词;工具结果 token 上限 4096(超大结果 8192)。
  • 摘要落 agent_session_summary,带边界(cutoff 时间 + 最后事件 ID)。
与短期记忆的分工:「会话持久化 + 摘要」解决「这一长串对话说了什么、该往回收缩」;「短期记忆(§8)」解决「跨会话还该记住什么」。两者互补,代码里也明确让框架 memory 负责预载、session 负责完整历史,避免重复注入。
设计思想(v2.0 新增):为什么非要 0.65 才触发压缩?为什么不是 0.8 或 0.5?详见 §15.5。

10飞书渠道集成

飞书是主要用户入口,代码在 agent_channel.go + 一组 agent_feishu_*.go

10.1 事件接收链路

飞书 → POST /agent/channel/feishu/event.json
   │
   ├─ 1. url_verification 挑战 → 原样返回 challenge
   ├─ 2. 签名校验(X-Lark-Signature)+ token 校验
   ├─ 3. 只看 im.message.receive_v1(其它事件忽略)
   ├─ 4. 群聊需 @ 机器人才响应(p2p 直接响应)
   ├─ 5. 事件去重(Redis,event_id/message_id 30 分钟)
   ├─ 6. 落库 agent_channel_event(pending)→ 异步入队
   └─ 7. worker 抢占(lease + 30s 心跳续租)→ 处理
      

10.2 处理与回复

  • 身份绑定:飞书 openID/userID/unionID → 查邮箱 → 反查后台账号(resolveFeishuAdminUID)。未绑定则 restricted。
  • 消息类型:text / image(转 base64 多模态)/ post(富文本,提取文本+图片)。
  • 体验优化:先发「思考中」卡片(sendFeishuThinkingReply),完成后原地更新卡片,或发最终卡片/文本;image_send 支持把产物图片发回。
  • 斜杠命令/help/new/stop/memory_view|pending|confirm|reject|delete|clear/skill_list/skill_invoke
  • 机器人菜单 + 长连接agent_feishu_menu.go(菜单/账号绑定页)、agent_feishu_long_conn.go(长连接主动推送)。
为什么 Chat 是非流式:就是为了这条飞书体验 —— 先发「思考中」,最终一次更新。如果流式,就无法优雅地「先占位再替换」。

11定时任务系统

让 Agent 从「被动对话」升级为「主动产出」,代码 agent_schedule.go

  • 类型once(一次性)/ cron(5 段 cron,分钟级,自研解析 parseCronSpec)。
  • 存储agent_schedule(任务)+ agent_schedule_run(每次执行记录)。
  • 扫描:cronjob ProcessDueSchedules,单轮最多并发 20(semaphore 闸门)。
  • 抢占ClaimDueid + updated_at 乐观锁,防止多进程重复执行;卡住的任务(processing 过久)会被重新捞起恢复。
ProcessDueSchedules(扫描到期任务)
   ├─ ListDue:enabled 且 next_run_at 到期 + processing 卡住待恢复
   ├─ ClaimDue(乐观锁抢占)→ 写 running 记录(agent_schedule_run)
   ├─ 复用 ChatSchedule(ScheduleExecution=true)
   │     SessionID = "schedule:{scheduleID}:{runID}"(按 run 隔离)
   │     prompt 注入「固定规则上下文快照 + 按任务时区解析的今天/昨天/明天日期」
   ├─ 模型必须调用 schedule_reply 工具 → 读取权威输出
   ├─ 有内容才投递到飞书(群聊 chat_id 或用户 open_id)
   └─ finishSchedule:更新状态 + 重算 next_run_at(cron 任务)
      
设计要点:定时任务每次干净执行——不复用跨次 cron 的会话历史、不写会话持久化,只依赖自包含 prompt + 创建时固化的固定规则快照。执行中禁用 schedule_* 工具,避免「任务又创建任务」。

12可观测性

Langfuse(OTEL Trace)

通过 StartLangfuseTelemetry 初始化,给每一轮 Chat 打 span:

  • langfuse.trace.name = Agent 名(或 traceName)
  • metadata:agent_id / llm_id / provider / model / admin_uid / turn_id / 飞书身份
  • user.id / session.id 关联 Langfuse 会话维度
  • 会话摘要单独开 span(命名 summarize {agent}

附带一个 V4 header 本地代理,给 SDK 补上 x-langfuse-ingestion-version: 4

结构化日志

统一 action 字段 + 分模块 tag:

  • agent_runtime —— Chat 编排主链路
  • agent_feishu_channel —— 飞书渠道
  • agent_short_term_memory —— 记忆
  • agent_schedule —— 定时任务

日志统一截断、密钥脱敏,避免刷爆或泄露。

13安全与权限

维度机制
调用身份public / admin / feishu / schedule 四类,restricted 调用裁掉全部敏感能力。
内部系统访问Omnibus 工具带当前用户 token;域名白名单/黑名单;写操作需 method+url+body 直连(已取消 pending 二次确认)。
Connectors 授权OAuth2 流程(state 防 CSRF、token 加密落库、过期自动刷新、运行时权限检查)。
凭证管理用户级 / Agent 级环境变量密文存储,仅执行时解密注入,日志/缓存 redact。
Skill 脚本仅执行「已加载 Skill」声明的脚本;路径禁止 ../绝对路径;超时+输出上限;参数与输出脱敏。
记忆与持久化落库/日志前 sanitize + redact 敏感字段。

14设计决策矩阵(升级版)

这一章把整套系统里的关键「设计决策」摆到台面,每条都给出选了 A、为什么不选 B、有什么代价。这是新人讲解和面试表达最值钱的内容。

D1. 三级缓存(LRU → Redis → MySQL)

涉及代码:agent_runtime_cache.go
选了
LRU(3min)→ Redis(30d)→ MySQL(永久)三级查找,每层都有失效广播
没选
只用 Redis(缺本地命中放大延迟);或两级(缺跨 Pod 复用)
代价
多一层缓存就有多一份不一致风险,必须配套 invalidate* 主动失效广播

D2. Skill 四来源(git / market / code_git / backend)

涉及代码:agent_skill.go / agent_skill_market.go / agent_code_git_skill.go
选了
Git 仓库(团队共建走 PR)+ Market(市场下载有版本)+ code_git(仓库内置)+ DB(快速实验),CompositeBackend 组合查询
没选
只走 Git(DB 改个实验脚本要 commit 太重);或只走 DB(脚本 / 大文档难维护)
代价
来源多 → 解析逻辑多份,调试时要知道「这个 Skill 是从哪个来源来的」

D3. 工具按场景裁剪(restricted / 普通 / 定时 / 明确 Skill)

涉及代码:buildRuntimeTools
选了
不同 invocation 看到不同工具集,restricted 调用工具面几乎为空
没选
统一一套工具 + 提示词限制(容易遗漏且模型会绕过);或在工具内部鉴权(每个调用都要查权限,延迟高)
代价
新增 invocation 类型要改裁剪逻辑;调试时要知道「这个工具为什么没出现」

D4. Memory + Session 分层(不合并)

涉及代码:agent_memory.go / agent_session_service.go
选了
Memory 跨会话抽「要点」(规则/偏好/经验)+ Session 完整历史(事件流 + 摘要)
没选
只有 Session(历史太长爆 token);或只有 Memory(丢失上下文细节)
代价
要明确分工避免重复注入(代码里用 WithPreloadMemory 避免短期记忆二次进 prompt)
D5. Chat 走非流式(WithStream(false)
涉及代码:agent.go
选了
非流式(一次返回完整回复),飞书先发「思考中」卡片 → 完成后替换
没选
流式(打字机效果好,但飞书卡片无法优雅占位替换)
代价
Web/公开 API 用户失去实时打字机感;改双通道需要非流式 + 流式并存

D6. 上下文压缩阈值 0.65 / 保留 2 轮

涉及代码:agent_context_compaction.go
选了
历史占比 ≥ 0.65 触发摘要,最近 2 轮不压;摘要 ≤ 800 词;工具结果上限 4096/8192 token
没选
固定消息数(如 30 条一刀切);或不压缩(爆 token 报错)
代价
阈值与窗口大小需要按业务调优,调太敏感会丢信息,调太迟会超上下文
更多设计思想见 §15 —— 6 大设计思想精讲,每个决策背后的「为什么这么做 + 不这么做会怎样 + 真实代价」都讲透。

这一章挑出 6 个最具代表性的设计决策,每个都给出问题 → 方案 → 取舍 → 代价 → 真实坑五个维度。这是新人讲解和面试表达的核心弹药库。

15.1 缓存分层:为什么是三级而不是两级?

15.2 Skill 分四种来源:为什么不统一成一种?

15.3 工具按场景裁剪:为什么不能一套工具走天下?

15.4 Memory 与 Session:为什么不合并成一个?

15.5 上下文压缩:为什么是 0.65 不是 0.8?

15.6 非流式 Chat:为什么放弃打字机效果?

四个真实业务 Agent(个人助理 / 客服排查 / 智能改图 / Smart 智能改图)看似各不相同,但抽象出来有同一个落地模板。讲清楚这个模板,就讲清了「如何用这个平台做一个新业务」。

16.1 共性:所有业务 Agent 都长一个样

16.2 如何用这个平台做一个新业务 Agent

16.3 真实案例对照(按模板解读)

案例 A:智能改图(factory/agentfactory/modifyImage.go

  • ②预处理:解析 effect_params → 校验 + 图片 URL 转签名 → 扣权益
  • ③路由:自动改图直接走 task;手动改图按 KV 路由(sop / task / eino / eino_group)
  • ④执行:异步任务 + SSE 持续推送进度
  • ⑤结果:异步回调写 Redis → ConsumeLoop 下发最终结果

设计亮点:手动 / 自动拆分、KV 路由不写死、多模态消息、失败兜底(权益回退 / 超时回退 / 断线重连)。

案例 B:Smart 智能改图(factory/agentfactory/smart.go

  • ②预处理:解析 SmartParams → 校验多人物 / 部位信息 → smart 次数预扣 → 预拉图片转 base64
  • ③路由:按人物 / 部位 / 子类目拆任务
  • ④执行:并发多 Agent 分析(runEinoAgent 多路)
  • ⑤结果:每部位提前推送 + 按部位写 Redis → 断线重连可重放

设计亮点:多人物并行分析、提示词含业务上下文(人脸框、客户端已识别问题)、天然适合做精细化推荐。

案例 C:客服排查 Agent

  • ②预处理:解析 gid / uid
  • ③路由:直接走 Eino Agent(Agent 自己决定调哪些 Skill)
  • ④执行:串行 / 并发调 5 类日志 Skill(业务 / 上传 / 处理 / 订阅 / 风控)
  • ⑤结果:汇总「失败原因 + 建议动作 + 归属部门」

设计亮点:把「查日志」升级为「判断责任边界」;Agent 自己编排 Skill 调用顺序;客服可先自助处置(误识别加白)。

案例 D:个人助理 Agent

  • ②预处理:加载用户上下文、定时任务快照
  • ③路由:直接走 Eino Agent
  • ④执行:用户主动提问 or 定时触发,同步 Eino Runner
  • ⑤结果:落短期记忆

设计亮点:把分散系统的重复性事务收敛;逐步叠加新 Skill 不改主流程;可定时主动推送(CDN 汇总、周报)。

列了 20 个面试里最可能被追问的「为什么」问题,每个都给出回答要点。背完这 20 题,应届生讲 AI Agent 项目基本不会被追问倒。

为什么用 Eino,不用 LangChain / AutoGen?
三个理由:语言匹配——整个项目是 Go,Eino 是字节 CloudWeGo 出品的 Go 原生 Agent 框架,没有跨语言调用成本;②生产级原语完整——llmagent / runner / session / memory / skill / tool 一套齐全,不需要像 LangChain 那样自己拼装;③字节内部生态——和公司其它 Go 服务(gRPC、kitex)天然兼容,部署运维统一。
三级缓存为什么是 LRU → Redis → MySQL,不是两级?
只有 Redis 不够(每次都走网络,毫秒级);只有本地 LRU 不够(多 Pod 不共享,重启冷启动打爆 DB)。三级组合:本地命中放大热点查询、Redis 跨 Pod 共享、MySQL 兜底强一致。详见 §15.1
本地缓存 TTL 3 分钟,Redis TTL 30 天,是怎么定的?
3 分钟够热(对话间隔一般 < 1min),又不至于「改配置 3min 内看不到」;30 天够长(绝大多数配置不会一个月不动),又能兜住重启冷启动。两者搭配 = 「人能容忍的延迟」+「最终一致兜底」。详见 §15.1
改了配置怎么保证缓存立即生效?
主动失效广播:改 Agent 触发 invalidateAgent(清 AgentConfig + AgentBundle);改 LLM 失效自身并 invalidateAllAgentBundles(因为很多 Agent 引用了该 LLM);改 Skill / 变量 / 仓库 / 市场同样广播失效。所有 invalidate 操作在保存配置的同一事务里同步执行。
Skill 为什么要分四种来源?统一成 Git 或 DB 不行吗?
不行:全走 Git,临时配个轻量 Skill 要 commit + push + 等同步,体验差;全走 DB,脚本 / 大文档难维护,代码 review 体验差。四种来源覆盖「团队共建 / 市场下载 / 仓库内置 / 临时实验」四种场景,用 CompositeBackend 组合查询:Git 优先(可追溯)→ DB 兜底(轻量灵活)。详见 §15.2
工具为什么按场景裁剪,不让模型「自己看着办」?
安全 + 成本双重考虑:①安全——prompt 软约束能被诱导绕过,工具面裁剪是结构性约束,模型根本看不到 = 不可能误用;②成本——工具多 = toolDefinitions 占几千 token,调用延迟高、容易选错。restricted 调用工具面几乎为空,从源头保证匿名用户无法越权。详见 §15.3
Memory 和 Session 为什么不合并?
职责不同:Memory 解决「跨会话还该记住什么」(规则 / 经验 / 偏好),Session 解决「这一长串对话说了什么」(完整事件流 + 摘要)。合并后要么 Session 表里塞满短文本 Memory(查询慢、占空间),要么 Memory 失效时连带会话历史都没了(数据丢失风险)。代码里用 WithPreloadMemory 明确分工,避免同一段短期记忆二次进 prompt。详见 §15.4
上下文压缩阈值为什么是 0.65,不是 0.8 或 0.5?
经验值。0.5 太早(上下文还富余就压,丢细节);0.9 太晚(留给本轮回复的 token 太少,可能截断)。0.65 留 ~35% 给本轮(用户消息 + 工具调用 + 回复),够用又不浪费。配合「保留最近 2 轮」不压缩,「最近意图」不丢。详见 §15.5
为什么 Chat 用非流式,不用流式(SSE 打字机)?
为了配合飞书体验:飞书卡片是原子替换,不支持「边生成边更新内容」。非流式 + 「思考中」占位卡片 → 完成后替换,体验可控。流式在飞书会变成「消息刷屏」或「卡片丢失占位」。代价是 Web 用户失去打字机感,未来可改进为双通道并存(飞书非流式、Web SSE 流式)。详见 §15.6
Agent 怎么防止工具死循环?
三层防护:①Eino 的 MaxIterations(最大循环轮数);②工具回调 newAgentToolCallbacksskill_script_execute 设调用次数上限(普通 4 次、定时 6 次);③工具结果压缩(把大段 stdout 裁到 4KB 摘要),让模型「拿不到完整信息就不会继续依赖工具」。
用户级环境变量(凭证)怎么存?怎么用?
agent_user_env / agent_env 表密文存储(加密 key 在服务端)。:仅在 Skill 脚本执行时解密注入到子进程环境变量 + AGENT_SKILL_ARGS_JSON不暴露给模型(模型通过工具调用即可,不需要直接拿密钥)。日志 / 缓存 redact,永远不打密钥原文。
飞书事件怎么保证不重复处理?
两级去重:①Redis 缓存 event_id / message_id 30 分钟,重复事件直接丢弃;②落库 agent_channel_event 表,主键 / 唯一索引兜底;③worker 抢占用 id + updated_at 乐观锁(ClaimDue 风格),即使多 Pod 也不会重复处理同一个事件。
定时任务怎么保证不重复执行?
ClaimDueid + updated_at 乐观锁:扫描到到期任务时,UPDATE 同时校验 updated_at 没变才抢占成功;抢占失败的任务下一轮重新扫描。卡住的任务(processing 过久)会被重新捞起恢复。SessionID = "schedule:{scheduleID}:{runID}" 按 run 隔离,避免历史污染。
为什么要做 restricted 调用?什么场景触发?
匿名公开 API + 未绑定后台账号的飞书用户——这两种调用没有可信身份,必须把 Agent 的 Network / Image / FeishuDoc / Omnibus / Memory / SessionPersistence 全部关掉。从源头保证匿名或未绑定用户无法越权操作内部系统,是结构性安全而不是 prompt 软约束。
Agent 怎么「带当前用户身份」调用内部系统?
三步:①飞书 / 后台入口先解析身份(adminUID / userID / 飞书 openID);②Omnibus 工具调用时自动带上当前用户 token(从 agent_user_env / Connectors token 取);③Connectors OAuth 在第一次访问时引导用户授权,后续自动刷新。模型不需要知道 token,只调工具即可。
为什么需要 Omnibus 网关,不能直接调内部 API?
Omnibus 是统一 API 网关,提供:①统一鉴权(一次接入,所有内部系统);②域名白名单 / 黑名单(Agent 不能乱调);③审计(所有调用可追溯);④限流。如果每个内部 API 都单独对接,Agent 要管理 N 套鉴权 + N 套错误码 = 不可维护。
Skill 脚本执行怎么保证安全?
四道防线:①白名单——只允许执行「当前 Agent 已加载 Skill」声明的脚本;②路径校验——禁止 .. / 绝对路径;③超时 + 输出上限——默认 300s,stdout/stderr 上限 64KB;④脱敏——参数和环境变量注入时解密但日志 redact;输出含本地路径替换为 [skill:...],密钥替换为 [redacted]。容器级沙箱隔离是未来改进方向。
怎么监控一个 Agent 的运行情况?
两个层次:①Langfuse trace——每一轮 Chat 一个 span,metadata 含 agent_id / llm_id / provider / model / admin_uid / turn_id / 飞书身份,会话摘要单独 span(命名 summarize {agent});②结构化日志——统一 action 字段 + 分模块 tag(agent_runtime / agent_feishu_channel / agent_short_term_memory / agent_schedule),统一截断 + 密钥脱敏。
怎么排查「Agent 答非所问」这种问题?
排查路径:①Langfuse 看 trace → 找到对应 turn → 看工具调用是否合理;②看 toolDefinitions 是否过多导致模型选错工具(按场景裁剪解决);③看短期记忆是否污染(MemoryWindow=3 太宽也会带噪音);④看上下文是否过长触发压缩(agent_session_summary 看摘要质量);⑤看 Skill 脚本输出是否被截断(64KB 上限)。从「工具 → 记忆 → 上下文 → Skill 输出」逐个排除。
这套系统最大的不足是什么?怎么改进?
不足:①非流式牺牲了 Web 体验(改:双通道并存);②三级缓存有多 Pod 一致性窗口(改:加 generation / 版本号);③Skill 脚本安全靠运行时约束,容器级隔离不够(改:Docker 沙箱 + 资源配额 + 调用频控);④关键指标监控不全(改:补加载失败率、工具失败率、任务耗时、token 用量);⑤新人上手成本高(改:完善文档 + 内部培训,就是这份文档的目标)。

18附录 / 数据表 / 术语 / 代码索引

A. 数据表清单(dao/daoagent)

表名用途
agent_configAgent 配置(prompt / llm_id / 开关 / skill_config_json)。
llm_configLLM 配置(provider / base_url / api_key / model / timeout)。
agent_channel_config渠道配置(飞书 app_id / secret / token / encrypt_key)。
agent_channel_event飞书事件队列(pending/processing/done/failed/ignored)。
agent_skill / agent_skill_repo / agent_skill_market / agent_skill_variableSkill 及其来源、市场、变量。
agent_memory_item / agent_memory_audit短期记忆 + 画像记忆,及治理审计。
agent_session / agent_session_event / agent_session_state / agent_session_summary / agent_session_turn会话、事件、状态、摘要、轮次。
agent_schedule / agent_schedule_run定时任务及执行记录。
agent_user_env / agent_env用户级 / Agent 级环境变量(凭证密文)。
agent_connectors_user_token / agent_omnibus_pending_actionConnectors token / Omnibus 待确认动作。

B. 术语表

术语说明
Eino云原生 Agent 框架,提供 llmagent / runner / session / memory / skill / tool 原语。
SkillAgent 的技能包(SKILL.md + manifest + 脚本 + 变量)。
RunnerEino 执行器,驱动「LLM 思考 → 工具调用 → 观察」循环。
Session / Memory会话(完整历史)与记忆(跨会话抽取的要点)。
Omnibus / Connectors内部系统统一 API 网关 / 授权平台。
restricted invocation受限调用(匿名/未绑定),关闭敏感能力。
Context Compaction上下文压缩,用摘要替换超长历史。
Explicit Skill明确 Skill —— 定时任务或 /skill 点名时只保留单个 Skill 的模式。
CompositeBackendSkill 多来源组合后端,Git 优先 → DB 兜底。
Lease / 乐观锁事件 / 任务的抢占机制,防止多 Pod 重复处理。

C. 关键代码路径索引

想看什么文件
一次对话主链路service/serviceAgent/agent.gochat()
三级缓存agent_runtime_cache.go
Skill 同步/解析agent_skill.go
Skill 脚本执行agent_skill_runtime.go
Skill 市场agent_skill_market.go
Skill 仓库内置agent_code_git_skill.go
记忆agent_memory.go / agent_memory_governance.go
会话持久化agent_session_service.go / agent_session_persistence_mysql.go
上下文压缩agent_context_compaction.go
飞书渠道agent_channel.go / agent_feishu_command.go / agent_feishu_long_conn.go
定时任务agent_schedule.go
Omnibus / Connectorsagent_omnibus_tool.go / agent_connectors_oauth.go
可观测agent_langfuse.go
智能改图 factoryfactory/agentfactory/modifyImage.go
Smart 智能改图 factoryfactory/agentfactory/smart.go

D. v2.0 新增章节速览

章节一句话价值
§14 设计决策矩阵(升级)6 条关键设计决策的「为什么 / 代价」对照表
§15 六大设计思想精讲(NEW)问题 → 方案 → 取舍 → 代价 → 真实坑,每个决策讲透
§16 业务 Agent 落地模板(NEW)所有业务 Agent 的共同模板 + 新业务 3 步走
§17 面试高频追问 Q&A(NEW)20 个「为什么」问题,应届生面试弹药库