AI Agent 平台架构说明 设计思想 v2.0
1系统概述与定位
这是一个 企业级 AI Agent 平台,核心代码位于 service/serviceAgent/(约 30k 行,含测试)。它的目标不是做一个「聊天机器人」,而是把 「大模型推理」和「企业内部系统的真实操作能力」连接起来——Agent 既能听懂自然语言,又能真的去查日志、读飞书文档、写内部系统、定时产出报表。
1.1 它到底解决了什么问题
- 把「多系统取数 + 规则汇总 + 自然语言输出」收敛成一句话:客服排查、周报、巡检、看板这些原本要人肉打开多个后台逐个截图、复制、汇总的活,交给 Agent 自动串起来。
- 让 Agent 带「当前用户身份」执行:查文档、调内部接口时能带上用户自己的 Token / 凭证 / 文档 ID(
agent_user_env/agent_env)。 - 把能力做成可插拔的 Skill 插件:新增一个能力 = 新增一个 Skill 包,不改主流程代码。
- 让「一次性对话」升级为「可持续的自动化」:既能实时对话,也能注册定时任务主动推送(
agent_schedule)。
1.2 核心价值
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 + Redis | MySQL 存配置/会话/记忆/定时任务;Redis 做三级缓存、会话热存、去重、任务 lease。 |
| 渠道 | 飞书 + 后台 + 公开 API | 飞书事件回调 + 长连接;后台 Admin 调试台;对外公开 Agent。 |
| 内部系统集成 | Omnibus API Gateway / Connectors | 通过 OAuth2 拿用户 token,经网关调用 Jira/Confluence/内部 API。 |
| 可观测 | Langfuse(OTEL)+ 结构化日志 | Trace 关联到每一轮 turn / 工具调用 / 模型调用。 |
| 脚本运行时 | python3 / node | Skill 脚本在服务端以子进程执行。 |
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.go | Agent/LLM/Skill/Channel/Env/Token 的「本地 LRU → Redis → MySQL」三级缓存与失效。 |
| Skill 系统 | agent_skill.go / agent_skill_runtime.go / agent_skill_market.go | Skill 同步、解析、加载、脚本执行。 |
| 记忆服务 | agent_memory.go / agent_memory_governance.go | 短期记忆 + 长期画像 + 治理(确认/审计)。 |
| 会话持久化 | agent_session_service.go / agent_session_persistence_mysql.go | Session/Event/State/Summary 落 MySQL + 内存热缓存。 |
| 上下文压缩 | agent_context_compaction.go | 会话摘要(Eino summarizer),超阈值自动压缩。 |
| 飞书渠道 | agent_channel.go / agent_feishu_*.go | 事件回调、签名校验、去重、身份绑定、回复/发图/命令/菜单/长连接。 |
| 定时任务 | agent_schedule.go | once/cron 任务扫描、抢占、执行、投递。 |
| 可观测 | agent_langfuse.go | Langfuse trace + 结构化日志。 |
| 内部集成 | agent_omnibus_tool.go / agent_connectors_*.go | Omnibus 网关工具 + 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)
| 入口 | 函数 | 身份 | 权限裁剪 |
|---|---|---|---|
| 公开 API | ChatPublic | 匿名(public:{turnID}) | 仅 PublicFlag=1 的 Agent;restricted:禁网络/图片/Omnibus/记忆/持久化 |
| 后台 | ChatAdmin | 登录后台账号 adminUID | 全量能力 |
| 飞书 | ChatFeishu | 飞书身份 → 邮箱 → 后台账号 | 未绑定账号则 restricted |
| 定时任务 | ChatSchedule | 任务创建时固化的身份 | ScheduleExecution=true:收窄工具、不走会话持久化 |
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 ──▶ 返回
| 缓存对象 | 本地 TTL | Redis TTL | 说明 |
|---|---|---|---|
| Agent Bundle | 3 分钟 | 30 天 | Agent + LLM + 渠道 + Skill 摘要 + Skill 指令,一次对话的核心快照。 |
| Skill Detail | 3 分钟 | 30 天 | Skill 元数据 + 仓库 + 市场 + 变量。 |
| Channel / Agent / LLM | 3 分钟 | 30 天 | 渠道配置、Agent 配置、LLM 配置。 |
| User Env / Agent Env | 3 分钟 | 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)。
[redacted]),不把密钥写进日志。6Skill 技能系统
Skill = 技能包,是 Agent 的「能力扩展单元」。一个 Skill 目录包含:
SKILL.md—— 技能描述(YAML frontmatter + 正文,告诉 AI 它能做什么)skill.yaml / skill.yml—— manifest:name / description / variables / scriptsscripts/或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 呼应)。 |
| 后端 DB | backend | 纯数据库存储的技能内容。 |
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」转成本轮给模型和工具使用的东西:
- 摘要注入 prompt:
buildSkillInstruction生成「# 已加载 Skill」清单(名称 + skill_id + repo + description + scripts + vars),有总量上限30KB。 - 框架技能工具:
llmagent.WithSkills(skillRepo)暴露skill_load / skill_list_docs / skill_select_docs,让模型按需加载 Skill 正文。 - 脚本执行工具:
skill_script_execute(见 §7)。
- 普通聊天:保留多个 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:...])+ 密钥脱敏。
7工具体系
工具由 buildRuntimeTools 按场景组装,不同调用场景看到的工具面不同——这是安全和控制成本的关键。
| 工具 | 用途 | 说明 |
|---|---|---|
skill_script_execute | 执行 Skill 脚本 | 只允许执行「当前 Agent 已加载 Skill」声明的脚本,路径安全校验(禁 ../绝对路径)。 |
omnibus | 读写内部系统 | 走 Omnibus 网关,带当前用户 token;GET/POST/PUT/PATCH/DELETE;域名白名单/黑名单校验。 |
| 网络工具 | 搜索 / 抓取网页 | 受 Agent 开关 + 域名白名单控制。 |
| 图片工具 | 图片分析 / 发图 | image(多模态理解)、image_send(把产物图片发回飞书)。 |
| 飞书工具 | 飞书文档 / 多维表格 | feishu_doc、feishu_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
newAgentToolCallbacks 里,对 skill_script_execute 做了调用次数限制和结果压缩(把大段 stdout 裁到 4KB 摘要,失败只保留定位字段),避免把脚本刷屏内容全量灌回模型;定时任务的 schedule_reply 返回后直接 SkipSummarization,省掉一次无意义的收尾 LLM 调用。8记忆系统
记忆分两层,全部落 agent_memory_item 表,通过 agent_memory.go 的 frameworkMemoryService 实现 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),避免任务执行时依赖「刚才聊过什么」。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)。
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(长连接主动推送)。
11定时任务系统
让 Agent 从「被动对话」升级为「主动产出」,代码 agent_schedule.go。
- 类型:
once(一次性)/cron(5 段 cron,分钟级,自研解析parseCronSpec)。 - 存储:
agent_schedule(任务)+agent_schedule_run(每次执行记录)。 - 扫描:cronjob
ProcessDueSchedules,单轮最多并发 20(semaphore 闸门)。 - 抢占:
ClaimDue用id + 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 任务)
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
D2. Skill 四来源(git / market / code_git / backend)
涉及代码:agent_skill.go / agent_skill_market.go / agent_code_git_skill.go
D3. 工具按场景裁剪(restricted / 普通 / 定时 / 明确 Skill)
涉及代码:buildRuntimeTools
D4. Memory + Session 分层(不合并)
涉及代码:agent_memory.go / agent_session_service.go
WithPreloadMemory 避免短期记忆二次进 prompt)D5. Chat 走非流式(WithStream(false))
涉及代码:agent.go
D6. 上下文压缩阈值 0.65 / 保留 2 轮
涉及代码:agent_context_compaction.go
15六大设计思想精讲 v2.0 NEW
这一章挑出 6 个最具代表性的设计决策,每个都给出问题 → 方案 → 取舍 → 代价 → 真实坑五个维度。这是新人讲解和面试表达的核心弹药库。
15.1 缓存分层:为什么是三级而不是两级?
▸问题
每次 Chat 要查 Agent 配置、LLM 配置、Skill 详情、渠道配置、用户环境变量、Token。如果每次都打 MySQL,对话延迟会从 100ms 涨到 300~500ms,高峰期 DB 压力大。
▸方案
三级缓存:
L1 本地 LRU TTL 3min —— 进程内命中,纳秒级
L2 Redis TTL 30天 —— 跨 Pod 共享,毫秒级
L3 MySQL 永久 —— 真源,强一致
▸为什么不是两级?
- 只有 Redis 一级:本地查不到必须走 Redis,多了 ~1ms 网络 RTT,高频 Chat 累计起来肉眼可感。
- 只有本地 LRU 一级:多 Pod 之间不共享,扩容 / 重启后缓存全部失效("冷启动风暴"),DB 瞬间被打爆。
- 三级组合:本地命中放大热点查询效率;Redis 让所有 Pod 共享一份;MySQL 兜底强一致。
▸TTL 为什么是 3 分钟 / 30 天?
- 本地 3 分钟:够热(一般对话间隔 < 1min),又不至于「改配置 3min 内看不到」。3 分钟和人的容忍度匹配。
- Redis 30 天:够长(绝大多数配置不会一个月不动),又能兜住重启后冷启动;30 天后强制回源 MySQL,保证最终一致。
▸代价
多级就有不一致窗口(最多 3 分钟),所以必须配套主动失效广播——改 Agent / LLM / Skill / Channel 都触发 invalidate*,确保「保存即生效」。如果忘了写失效逻辑,配置改了看起来没生效。
15.2 Skill 分四种来源:为什么不统一成一种?
▸问题
Skill 是 Agent 的能力扩展单元,但「能力从哪儿来」在团队里是多样的:
- 团队成员在自己 Git 仓库里改 Skill → 要走 PR 评审
- 从 Skill 市场下载现成的 → 要有版本管理
- 开发调试期临时写个 Skill → 不该污染主仓库
- 某些轻量 Skill 只是几行脚本 → 不值得开仓库
▸方案
四种来源 + CompositeBackend:
| 来源 | 定位 | 典型场景 |
|---|---|---|
git | 独立 Git 仓库 | 团队正式共建的 Skill |
market | Skill 市场下载 | 复用一个社区/官方 Skill |
code_git | 项目代码仓库 .agents/skills/ | 跟业务代码一起演进 |
backend | 数据库直接存 | 临时实验、运营人员轻量配置 |
加载时用 CompositeBackend:Git 优先(可追溯)→ DB 兜底(轻量灵活)。
▸为什么不统一成一种?
- 全走 Git:临时配个轻量 Skill 要 commit + push + 等同步,体验差。
- 全走 DB:脚本 / references 等文件型资产难维护,代码 review 体验差。
- 四来源:每种场景用最合适的形态,新增 Skill 时不用纠结「要不要建仓库」。
▸代价
来源多 → 解析逻辑多份;调试时必须知道「这个 Skill 是从哪个来源来的」(靠 SourceType 字段判断)。新人上手成本高,但长期看灵活度更高。
15.3 工具按场景裁剪:为什么不能一套工具走天下?
▸问题
Agent 可能调用:网络搜索、Omnibus 内部 API、飞书读写、图片分析、Skill 脚本执行……如果所有 invocation 都看到同一套工具,会出两个问题:
- 安全问题:匿名调用也能看到 Omnibus,模型一旦被诱导就能调内部接口。
- 成本问题:工具多了,模型每次要选,
toolDefinitions占几千 token,调用延迟高、容易选错。
▸方案
四种工具面:
普通聊天 → 全量工具(除 schedule_reply)
定时任务 → 收窄到 schedule_reply + skill_script_execute
明确 Skill → 仅当前 Skill 的脚本 + schedule_reply
restricted → 工具面几乎为空(关网络/图片/Omnibus/记忆/持久化)
▸为什么不让模型「自己看着办」?
- 模型在 prompt 里再写一遍「禁止调用 X」也是软约束,被诱导就破。
- 在工具内部鉴权 = 每次调用都查一次权限,延迟 + 几 ms 累加起来很大。
- 从源头裁剪 = 模型根本看不到这个工具 = 不可能误用 = 既安全又快。
▸代价
- 新增 invocation 类型要改裁剪逻辑。
- 调试时模型「看不到某个工具」,第一反应要查
buildRuntimeTools而不是 prompt。
15.4 Memory 与 Session:为什么不合并成一个?
▸问题
对话历史要存、用户偏好要存、技能规则要存。是不是一个大表全存就行?
▸方案:分两层,各管一段
| Memory(记忆) | Session(会话) | |
|---|---|---|
| 存什么 | 跨会话抽出的「要点」(规则 / 经验 / 偏好) | 完整对话事件流 + 摘要 |
| 数量上限 | 短期 100 条 / 长期 50 条 | 不限,按需存储 |
| 预载给模型 | 短期 MemoryWindow=3,长期 5 条 | 走 Eino framework session,按事件流加载 |
| 典型应用 | 「用户喜欢简洁回答」「查询之前先校验权限」 | 「上一轮我说想看北京的天气」 |
| 谁来管 | frameworkMemoryService | session.Service(Eino 框架实现) |
▸为什么不合并?
- 职责不同:Memory 解决「跨会话还该记住什么」,Session 解决「这一长串对话说了什么」。
- 压缩策略不同:Memory 走自动抽取 + 确认机制,Session 走摘要压缩 + 完整历史回放。
- 注入时机不同:Memory 用
WithPreloadMemory走框架预载,Session 走完整事件流重建。 - 合并后:要么 Session 表里塞满短文本 Memory(查询慢、占空间),要么 Memory 失效时连带会话历史都没了(数据丢失风险)。
▸关键代码细节
代码里用 WithPreloadMemory 明确分工,避免同一段短期记忆同时通过 messages 和 memory 重复进 prompt(重复注入会让模型困惑,也浪费 token)。
15.5 上下文压缩:为什么是 0.65 不是 0.8?
▸问题
多轮对话历史会无限增长,最终撑爆 LLM 上下文窗口(报错或被截断)。怎么办?
▸方案:阈值 + 摘要 + 保留最近
历史占上下文窗口比例 ≥ 0.65 ──触发──▶ 摘要旧历史(保留最近 2 轮)
摘要 ≤ 800 词 ──落库──▶ agent_session_summary
工具结果上限 4096 / 8192 token ──裁剪──▶ 超大结果降级
▸为什么是 0.65?
- 太早(如 0.5):上下文还富余就压,丢掉细节;摘要也是 LLM 调用,多花成本。
- 太晚(如 0.9):留给本轮回复的 token 太少,可能出现「截断 / 答非所问」。
- 0.65 的经验值:留 ~35% 给本轮(用户消息 + 工具调用 + 回复),够用又不浪费。
▸为什么「保留最近 2 轮」?
用户的「最近意图」最值钱,压缩时一定要保留。「2 轮」是经验值(4 条消息:用户 → 助手 → 用户 → 助手),够理解「他刚才在问什么」,又不会让 token 爆。
▸工具结果单独限流
工具调用结果是上下文最大的来源(脚本可能吐出几十 KB)。4096 token 是常规上限,超大工具(涉及大批数据)放宽到 8192,但仍要截断 + 摘要,避免一次工具调用炸掉上下文。
▸代价
阈值和窗口是经验参数,不同业务可能需要调。调太敏感会丢信息,调太迟会爆。需要在线监控 token 用量慢慢调。
15.6 非流式 Chat:为什么放弃打字机效果?
▸问题
主流 LLM 都支持流式输出(token-by-token),用户体验上能「边想边吐字」,看起来更聪明。本系统的 Chat 却选了非流式(WithStream(false)),原因是什么?
▸方案
非流式:模型想完整 → 一次性返回 → 飞书先发「思考中」卡片 → 完成后替换卡片。
▸为什么不流式?
- 飞书卡片体验:飞书消息支持「卡片消息」,但卡片在客户端是原子替换的,不支持「边生成边更新内容」。如果流式,要么先发文本(卡片丢失占位效果),要么每收到一段就发一张新卡片(消息刷屏)。
- 工具调用等待:Agent 在循环里要调工具(脚本跑几秒、调 API 几十 ms),流式期间要「停下来等工具结果」,用户视角会出现一段段停顿,反而更难受。
- 非流式 + 占位卡片:用户看到「思考中」→ 等待 → 一次看到完整答案,体验其实是可控的。
▸代价
Web / 公开 API 用户失去打字机感(要等几秒才看到第一行)。未来可改进为双通道并存:飞书用非流式 + 占位卡片,Web 用 SSE 流式 + 打字机。
▸教训
技术选型不能只看「主流 / 默认」,要回到业务场景。流式是 LLM 默认行为,但不是所有 Agent 体验都需要。判断依据:渠道对「中途增量更新」是否友好。
16业务 Agent 落地模板 v2.0 NEW
四个真实业务 Agent(个人助理 / 客服排查 / 智能改图 / Smart 智能改图)看似各不相同,但抽象出来有同一个落地模板。讲清楚这个模板,就讲清了「如何用这个平台做一个新业务」。
16.1 共性:所有业务 Agent 都长一个样
▸五段式标准结构
① 入口校验(参数解析、身份校验、限流)
│
▼
② 参数预处理(图片签名 URL、参数校验、权益预扣减)
│
▼
③ 路由分发(按场景选执行路径:自动改图/手动改图/SOP/Eino Agent)
│
▼
④ 执行(异步任务 + SSE 进度推送 OR 同步调用 + 流式事件)
│
▼
⑤ 结果处理(回调落 Redis → ConsumeLoop 下发 → 写记忆/落会话)
四个业务 Agent 都遵循这个模板,区别只在「②-④-⑤」具体实现:
| 业务 | ②预处理 | ③路由 | ④执行 | ⑤结果 |
|---|---|---|---|---|
| 个人助理 | 加载用户上下文、加载定时任务 | 直接走 Agent | 同步 Eino Runner | 落短期记忆 |
| 客服排查 | 解析 gid / uid | 走多 Skill 编排 | 同步查询多系统 | 汇总原因 + 部门归属 |
| 智能改图 | 图片签名 + 扣权益 | KV 配置路由(sop / task / eino / eino_group) | 异步任务 + SSE 进度 | 回调 Redis → 下发 |
| Smart 改图 | smart 次数预扣 + 图片预拉 base64 | 按人物/部位拆任务 | 并发多 Agent 分析 | 按部位写 Redis + 提前推送 |
16.2 如何用这个平台做一个新业务 Agent
▸三步走流程
- 在后台配置一个 Agent(
agent_config):填指令 prompt、选 LLM、配 Skill 清单、配工具权限。 - (可选)创建对应 Skill:如果业务需要调用内部系统,把「读某系统 / 写某系统」封装成 Skill(含 SKILL.md + 脚本)。
- 选入口:飞书 / 后台 / 公开 API / 定时任务,按需选一个或多个。
完全不用动主流程代码——这就是「插件化」的价值:新增业务 = 后台配置,不改主代码。
▸如果遇到这三种情况,需要改代码
- 业务有特殊预处理(如权益预扣减、图片转 base64)→ 在
factory/agentfactory/加一个 factory 文件。 - 业务有特殊路由策略(如 KV 动态路由)→ 同上,加 factory。
- 业务有特殊结果处理(如按部位写 Redis)→ 在 Skill 脚本里实现,或加新的 worker。
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 汇总、周报)。
17面试高频追问 Q&A v2.0 NEW
列了 20 个面试里最可能被追问的「为什么」问题,每个都给出回答要点。背完这 20 题,应届生讲 AI Agent 项目基本不会被追问倒。
invalidateAgent(清 AgentConfig + AgentBundle);改 LLM 失效自身并 invalidateAllAgentBundles(因为很多 Agent 引用了该 LLM);改 Skill / 变量 / 仓库 / 市场同样广播失效。所有 invalidate 操作在保存配置的同一事务里同步执行。
CompositeBackend 组合查询:Git 优先(可追溯)→ DB 兜底(轻量灵活)。详见 §15.2。
toolDefinitions 占几千 token,调用延迟高、容易选错。restricted 调用工具面几乎为空,从源头保证匿名用户无法越权。详见 §15.3。
WithPreloadMemory 明确分工,避免同一段短期记忆二次进 prompt。详见 §15.4。
MaxIterations(最大循环轮数);②工具回调 newAgentToolCallbacks 对 skill_script_execute 设调用次数上限(普通 4 次、定时 6 次);③工具结果压缩(把大段 stdout 裁到 4KB 摘要),让模型「拿不到完整信息就不会继续依赖工具」。
agent_user_env / agent_env 表密文存储(加密 key 在服务端)。用:仅在 Skill 脚本执行时解密注入到子进程环境变量 + AGENT_SKILL_ARGS_JSON,不暴露给模型(模型通过工具调用即可,不需要直接拿密钥)。日志 / 缓存 redact,永远不打密钥原文。
event_id / message_id 30 分钟,重复事件直接丢弃;②落库 agent_channel_event 表,主键 / 唯一索引兜底;③worker 抢占用 id + updated_at 乐观锁(ClaimDue 风格),即使多 Pod 也不会重复处理同一个事件。
ClaimDue 用 id + updated_at 乐观锁:扫描到到期任务时,UPDATE 同时校验 updated_at 没变才抢占成功;抢占失败的任务下一轮重新扫描。卡住的任务(processing 过久)会被重新捞起恢复。SessionID = "schedule:{scheduleID}:{runID}" 按 run 隔离,避免历史污染。
agent_user_env / Connectors token 取);③Connectors OAuth 在第一次访问时引导用户授权,后续自动刷新。模型不需要知道 token,只调工具即可。
.. / 绝对路径;③超时 + 输出上限——默认 300s,stdout/stderr 上限 64KB;④脱敏——参数和环境变量注入时解密但日志 redact;输出含本地路径替换为 [skill:...],密钥替换为 [redacted]。容器级沙箱隔离是未来改进方向。
summarize {agent});②结构化日志——统一 action 字段 + 分模块 tag(agent_runtime / agent_feishu_channel / agent_short_term_memory / agent_schedule),统一截断 + 密钥脱敏。
toolDefinitions 是否过多导致模型选错工具(按场景裁剪解决);③看短期记忆是否污染(MemoryWindow=3 太宽也会带噪音);④看上下文是否过长触发压缩(agent_session_summary 看摘要质量);⑤看 Skill 脚本输出是否被截断(64KB 上限)。从「工具 → 记忆 → 上下文 → Skill 输出」逐个排除。
18附录 / 数据表 / 术语 / 代码索引
A. 数据表清单(dao/daoagent)
| 表名 | 用途 |
|---|---|
agent_config | Agent 配置(prompt / llm_id / 开关 / skill_config_json)。 |
llm_config | LLM 配置(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_variable | Skill 及其来源、市场、变量。 |
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_action | Connectors token / Omnibus 待确认动作。 |
B. 术语表
| 术语 | 说明 |
|---|---|
| Eino | 云原生 Agent 框架,提供 llmagent / runner / session / memory / skill / tool 原语。 |
| Skill | Agent 的技能包(SKILL.md + manifest + 脚本 + 变量)。 |
| Runner | Eino 执行器,驱动「LLM 思考 → 工具调用 → 观察」循环。 |
| Session / Memory | 会话(完整历史)与记忆(跨会话抽取的要点)。 |
| Omnibus / Connectors | 内部系统统一 API 网关 / 授权平台。 |
| restricted invocation | 受限调用(匿名/未绑定),关闭敏感能力。 |
| Context Compaction | 上下文压缩,用摘要替换超长历史。 |
| Explicit Skill | 明确 Skill —— 定时任务或 /skill 点名时只保留单个 Skill 的模式。 |
| CompositeBackend | Skill 多来源组合后端,Git 优先 → DB 兜底。 |
| Lease / 乐观锁 | 事件 / 任务的抢占机制,防止多 Pod 重复处理。 |
C. 关键代码路径索引
| 想看什么 | 文件 |
|---|---|
| 一次对话主链路 | service/serviceAgent/agent.go(chat()) |
| 三级缓存 | 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 / Connectors | agent_omnibus_tool.go / agent_connectors_oauth.go |
| 可观测 | agent_langfuse.go |
| 智能改图 factory | factory/agentfactory/modifyImage.go |
| Smart 智能改图 factory | factory/agentfactory/smart.go |
D. v2.0 新增章节速览
| 章节 | 一句话价值 |
|---|---|
| §14 设计决策矩阵(升级) | 6 条关键设计决策的「为什么 / 代价」对照表 |
| §15 六大设计思想精讲(NEW) | 问题 → 方案 → 取舍 → 代价 → 真实坑,每个决策讲透 |
| §16 业务 Agent 落地模板(NEW) | 所有业务 Agent 的共同模板 + 新业务 3 步走 |
| §17 面试高频追问 Q&A(NEW) | 20 个「为什么」问题,应届生面试弹药库 |