走向 Memory OS:企业私有化 Agent 设计与实现

走向 Memory OS:企业私有化 Agent 设计与实现

作 者:吴佳浩(Alben)

微某信公某众号:全栈架构师笔记

系列专栏:《打造下一代智能体:企业级 Coding / DevOps Agent》· 第 03 篇


导读

单个 Agent 只是数字员工,拥有统一心智、共享工具与共同工作流的 Agent 网络才像一个组织。

企业真正缺的不是更多聊天机器人,而是一套能够统一管理 Agent 身份、工具、记忆、工作流和审计的运行平台。

模型决定 Agent 能思考什么,平台决定 Agent 能否在企业里安全工作;Memory OS 保存的不是聊天记录,而是组织持续积累的判断与经验。


在前两篇中,我们分别拆解了 Coding Agent 和 DevOps 排错 Agent 的工程闭环:

  • 🔸 Coding Agent 进入代码仓库,读取文件、修改 Patch、运行测试;
  • 🔸 DevOps Agent 接收告警,检索日志、定位根因、创建分支并生成 PR。

当企业只运行一个 Agent 时,这种单体架构足够直接。真正的麻烦出现在 Agent 数量增加之后:Coding Agent、Office Agent、CRM Agent、DevOps Agent 和 Security Agent 各自拥有一套工具、权限、会话和记忆。

用户在 CRM Agent 中确认了客户的技术约束,销售 Agent 不知道;DevOps Agent 刚刚处理完一次线上故障,Coding Agent 下一次仍然会重复踩坑;同一个 GitLab 工具被多个业务线重复接入,却没有统一的权限和审计策略。

text 复制代码
孤立 Agent × N
       │
       ├── 各自的工具注册表
       ├── 各自的会话与 Memory
       ├── 各自的权限与审计
       └── 各自的 Token 账单

结果:Agent 数量增加,治理复杂度按网络关系增长

这不是"多部署几个服务"就能解决的问题,而是 Agent 架构从应用组件走向平台基础设施的分水岭。

真正需要平台化的,不是 Agent 的数量,而是多个 Agent 开始共同读写同一批用户、项目、工具和业务状态。


一、为什么企业不能为每个 Agent 单独造一套基础设施?

企业在 Agent 规模化之后会遇到四类结构性问题。

问题 典型表现 真正的架构原因
认知割裂 客服知道客户偏好,销售和交付不知道 Memory 属于单个 Agent 进程,没有组织级共享状态
工具重复 多个团队重复接入 Git、Jira、数据库 工具没有统一的注册、授权、版本与审计平面
权限失控 Agent 继承了过大的用户权限 身份、租户、Agent 角色和工具权限没有分层
成本失控 多个 Agent 重复检索、重复总结、重复调用模型 没有统一的预算、缓存、限流和成本归因

企业需要的不是一个更大的 Agent,而是一层新的基础设施:
#mermaid-svg-qd7DWSEZNpACMfB2{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-qd7DWSEZNpACMfB2 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-qd7DWSEZNpACMfB2 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-qd7DWSEZNpACMfB2 .error-icon{fill:#552222;}#mermaid-svg-qd7DWSEZNpACMfB2 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-qd7DWSEZNpACMfB2 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-qd7DWSEZNpACMfB2 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-qd7DWSEZNpACMfB2 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-qd7DWSEZNpACMfB2 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-qd7DWSEZNpACMfB2 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-qd7DWSEZNpACMfB2 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-qd7DWSEZNpACMfB2 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-qd7DWSEZNpACMfB2 .marker.cross{stroke:#333333;}#mermaid-svg-qd7DWSEZNpACMfB2 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-qd7DWSEZNpACMfB2 p{margin:0;}#mermaid-svg-qd7DWSEZNpACMfB2 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-qd7DWSEZNpACMfB2 .cluster-label text{fill:#333;}#mermaid-svg-qd7DWSEZNpACMfB2 .cluster-label span{color:#333;}#mermaid-svg-qd7DWSEZNpACMfB2 .cluster-label span p{background-color:transparent;}#mermaid-svg-qd7DWSEZNpACMfB2 .label text,#mermaid-svg-qd7DWSEZNpACMfB2 span{fill:#333;color:#333;}#mermaid-svg-qd7DWSEZNpACMfB2 .node rect,#mermaid-svg-qd7DWSEZNpACMfB2 .node circle,#mermaid-svg-qd7DWSEZNpACMfB2 .node ellipse,#mermaid-svg-qd7DWSEZNpACMfB2 .node polygon,#mermaid-svg-qd7DWSEZNpACMfB2 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-qd7DWSEZNpACMfB2 .rough-node .label text,#mermaid-svg-qd7DWSEZNpACMfB2 .node .label text,#mermaid-svg-qd7DWSEZNpACMfB2 .image-shape .label,#mermaid-svg-qd7DWSEZNpACMfB2 .icon-shape .label{text-anchor:middle;}#mermaid-svg-qd7DWSEZNpACMfB2 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-qd7DWSEZNpACMfB2 .rough-node .label,#mermaid-svg-qd7DWSEZNpACMfB2 .node .label,#mermaid-svg-qd7DWSEZNpACMfB2 .image-shape .label,#mermaid-svg-qd7DWSEZNpACMfB2 .icon-shape .label{text-align:center;}#mermaid-svg-qd7DWSEZNpACMfB2 .node.clickable{cursor:pointer;}#mermaid-svg-qd7DWSEZNpACMfB2 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-qd7DWSEZNpACMfB2 .arrowheadPath{fill:#333333;}#mermaid-svg-qd7DWSEZNpACMfB2 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-qd7DWSEZNpACMfB2 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-qd7DWSEZNpACMfB2 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qd7DWSEZNpACMfB2 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-qd7DWSEZNpACMfB2 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qd7DWSEZNpACMfB2 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-qd7DWSEZNpACMfB2 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-qd7DWSEZNpACMfB2 .cluster text{fill:#333;}#mermaid-svg-qd7DWSEZNpACMfB2 .cluster span{color:#333;}#mermaid-svg-qd7DWSEZNpACMfB2 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-qd7DWSEZNpACMfB2 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-qd7DWSEZNpACMfB2 rect.text{fill:none;stroke-width:0;}#mermaid-svg-qd7DWSEZNpACMfB2 .icon-shape,#mermaid-svg-qd7DWSEZNpACMfB2 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qd7DWSEZNpACMfB2 .icon-shape p,#mermaid-svg-qd7DWSEZNpACMfB2 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-qd7DWSEZNpACMfB2 .icon-shape .label rect,#mermaid-svg-qd7DWSEZNpACMfB2 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qd7DWSEZNpACMfB2 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-qd7DWSEZNpACMfB2 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-qd7DWSEZNpACMfB2 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} User / Application
Agent Gateway

身份 · 租户 · 配额 · 审计
Agent Runtime

Planning · Reasoning · Conversation Loop
Tool Plane

MCP Gateway · Tool Registry · Sandbox
Memory Plane

Recall · Write-back · Reflection · Forget
Workflow Plane

DAG · FSM · Blackboard · Subagents
Enterprise Data Plane
PostgreSQL
Vector Store
Graph Store
Object Storage

这套平台至少要把六类职责分开:

  • 🔸 Agent Gateway:处理身份认证、租户解析、权限决策、限流和审计;
  • 🔸 Agent Runtime:负责单次任务的规划、推理、上下文组装和循环控制;
  • 🔸 Tool Plane:统一管理 MCP Server、Tool Schema、沙箱和人工审批;
  • 🔸 Memory Plane:统一处理 Recall、Extraction、Write-back、Reflection 和 Retention;
  • 🔸 Workflow Plane:负责 DAG、FSM、Subagent 和共享黑板;
  • 🔸 Data Plane:承载结构化状态、向量、关系图和历史归档。

Agent Platform 和 Memory OS 不是一回事

这两个概念经常被放在一起,但职责不同:

概念 管理对象 核心问题
Agent Platform Agent、模型、工具、工作流、运行实例 Agent 如何运行?
Memory OS 主体认知、任务状态、Skill、事件证据 Agent 如何保持连续性?

可以用一句话划分边界:

Agent Platform 管理执行,Memory OS 管理连续性。
一句话总结这一章的核心观点:

Agent 数量增加之后,企业需要治理的不是更多 Prompt,而是 Agent 运行时周围的基础设施。


二、什么时候不应该建设 Memory OS?

平台化不是技术信仰,也不是架构炫技。

如果业务只有一个 Agent、一个用户、一个会话,并且没有跨会话状态共享,那么引入 Memory Gateway、Policy Engine、向量数据库和图数据库,只会把简单问题变复杂。

业务场景 推荐方案 不需要的能力
单 Agent、单用户、无跨会话状态 进程内对象、SQLite 或本地文件 Memory Service、跨 Agent 共享
只查企业制度和产品文档 RAG + 向量检索 Profile 演进、Reflection、共享黑板
单团队共享少量经验 Team Memory Service 企业级 Graph、多租户物理隔离
多 Agent 共享项目状态 Memory Platform 复杂组织级 Memory OS 能力可以延后
多租户、跨团队、需要审计合规 Enterprise Memory OS 不建议继续使用进程内 Memory

真正的判断标准不是"我们有多少个 Agent",而是下面三个问题:

  • 🔸 多个 Agent 是否会读取同一个用户或项目的状态?
  • 🔸 一个 Agent 的写入是否会改变另一个 Agent 的后续决策?
  • 🔸 企业是否需要对 Memory 做权限撤回、审计追踪和合规删除?

如果三个问题全部回答"否",Memory OS 大概率是过度设计。

如果其中两个以上回答"是",继续让每个 Agent 自己维护 Memory,迟早会形成数据孤岛。

一句话总结这一章的核心观点:

没有跨 Agent 状态共享需求时,Memory OS 是过度设计;多个 Agent 开始共同改变业务状态时,Memory OS 才变成基础设施。


三、Memory OS 到底管理什么?

Memory OS 不是一个更大的向量数据库,也不是所有对话的归档盘。

它真正管理的是 Agent 在组织中的四类长期状态:

Memory OS 记忆类型 管理内容
Subject Memory 用户、团队、客户、组织的主体认知
Task Memory 项目目标、阶段状态、决策与待办
Procedural Memory SOP、Skill、排错规程与验证过的经验
Event Memory 告警、工具执行、失败轨迹与审计事件

这四类状态不能使用同一种生命周期:

状态类型 典型内容 默认作用域 生命周期
Subject Memory 用户偏好、团队规范、客户约束 User / Team / Tenant 长期,允许显式修改
Task Memory 当前项目、PR、事故处理进度 Task / Session 随任务结束归档
Procedural Memory Skill、SOP、排错方法 Agent / Team / Tenant 版本化,需要验证后发布
Event Memory Tool 调用、日志、失败轨迹 Audit / Task 按审计和合规策略保留

Profile、Skill 和 Event 不能混成一张向量表。

Profile 是约束,Skill 是方法,Event 是证据。把三者混在一起,召回结果必然会把"应该遵守的规则"和"曾经发生过的偶然事件"混为一谈。

四类 Memory 的写入来源不同

#mermaid-svg-vUCx2CBtnGp3Bk7R{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-vUCx2CBtnGp3Bk7R .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-vUCx2CBtnGp3Bk7R .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-vUCx2CBtnGp3Bk7R .error-icon{fill:#552222;}#mermaid-svg-vUCx2CBtnGp3Bk7R .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-vUCx2CBtnGp3Bk7R .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-vUCx2CBtnGp3Bk7R .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-vUCx2CBtnGp3Bk7R .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-vUCx2CBtnGp3Bk7R .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-vUCx2CBtnGp3Bk7R .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-vUCx2CBtnGp3Bk7R .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-vUCx2CBtnGp3Bk7R .marker{fill:#333333;stroke:#333333;}#mermaid-svg-vUCx2CBtnGp3Bk7R .marker.cross{stroke:#333333;}#mermaid-svg-vUCx2CBtnGp3Bk7R svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-vUCx2CBtnGp3Bk7R p{margin:0;}#mermaid-svg-vUCx2CBtnGp3Bk7R .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-vUCx2CBtnGp3Bk7R .cluster-label text{fill:#333;}#mermaid-svg-vUCx2CBtnGp3Bk7R .cluster-label span{color:#333;}#mermaid-svg-vUCx2CBtnGp3Bk7R .cluster-label span p{background-color:transparent;}#mermaid-svg-vUCx2CBtnGp3Bk7R .label text,#mermaid-svg-vUCx2CBtnGp3Bk7R span{fill:#333;color:#333;}#mermaid-svg-vUCx2CBtnGp3Bk7R .node rect,#mermaid-svg-vUCx2CBtnGp3Bk7R .node circle,#mermaid-svg-vUCx2CBtnGp3Bk7R .node ellipse,#mermaid-svg-vUCx2CBtnGp3Bk7R .node polygon,#mermaid-svg-vUCx2CBtnGp3Bk7R .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-vUCx2CBtnGp3Bk7R .rough-node .label text,#mermaid-svg-vUCx2CBtnGp3Bk7R .node .label text,#mermaid-svg-vUCx2CBtnGp3Bk7R .image-shape .label,#mermaid-svg-vUCx2CBtnGp3Bk7R .icon-shape .label{text-anchor:middle;}#mermaid-svg-vUCx2CBtnGp3Bk7R .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-vUCx2CBtnGp3Bk7R .rough-node .label,#mermaid-svg-vUCx2CBtnGp3Bk7R .node .label,#mermaid-svg-vUCx2CBtnGp3Bk7R .image-shape .label,#mermaid-svg-vUCx2CBtnGp3Bk7R .icon-shape .label{text-align:center;}#mermaid-svg-vUCx2CBtnGp3Bk7R .node.clickable{cursor:pointer;}#mermaid-svg-vUCx2CBtnGp3Bk7R .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-vUCx2CBtnGp3Bk7R .arrowheadPath{fill:#333333;}#mermaid-svg-vUCx2CBtnGp3Bk7R .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-vUCx2CBtnGp3Bk7R .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-vUCx2CBtnGp3Bk7R .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vUCx2CBtnGp3Bk7R .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-vUCx2CBtnGp3Bk7R .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vUCx2CBtnGp3Bk7R .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-vUCx2CBtnGp3Bk7R .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-vUCx2CBtnGp3Bk7R .cluster text{fill:#333;}#mermaid-svg-vUCx2CBtnGp3Bk7R .cluster span{color:#333;}#mermaid-svg-vUCx2CBtnGp3Bk7R div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-vUCx2CBtnGp3Bk7R .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-vUCx2CBtnGp3Bk7R rect.text{fill:none;stroke-width:0;}#mermaid-svg-vUCx2CBtnGp3Bk7R .icon-shape,#mermaid-svg-vUCx2CBtnGp3Bk7R .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vUCx2CBtnGp3Bk7R .icon-shape p,#mermaid-svg-vUCx2CBtnGp3Bk7R .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-vUCx2CBtnGp3Bk7R .icon-shape .label rect,#mermaid-svg-vUCx2CBtnGp3Bk7R .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vUCx2CBtnGp3Bk7R .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-vUCx2CBtnGp3Bk7R .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-vUCx2CBtnGp3Bk7R :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户输入
Subject Memory

偏好 · 约束 · 画像
Workflow 状态
Task Memory

目标 · 决策 · 待办
验证通过的排错轨迹
Procedural Memory

Skill · SOP · 规程
Tool / API / Alert 事件
Event Memory

证据 · 审计 · 轨迹
Memory Runtime
受权限控制的 Recall

  • 🔸 用户明确表达的稳定偏好,才有资格进入 Subject Memory;
  • 🔸 工作流中的中间状态,优先进入 Task Memory,不应该直接提升为长期事实;
  • 🔸 只有经过测试或人工确认的成功经验,才可以发布为 Procedural Memory;
  • 🔸 Event Memory 是证据来源,不能自动等价为事实。

一句话总结这一章的核心观点:

Memory OS 管理的不是聊天记录,而是主体认知、任务状态、程序性技能和事件证据。


四、平台的核心不是 API,而是控制平面

Memory OS 的数据平面可以使用 PostgreSQL、向量数据库、图数据库和对象存储,但真正决定平台质量的是控制平面。
#mermaid-svg-RL5tqbqnuy7v27tY{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-RL5tqbqnuy7v27tY .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-RL5tqbqnuy7v27tY .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-RL5tqbqnuy7v27tY .error-icon{fill:#552222;}#mermaid-svg-RL5tqbqnuy7v27tY .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-RL5tqbqnuy7v27tY .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-RL5tqbqnuy7v27tY .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-RL5tqbqnuy7v27tY .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-RL5tqbqnuy7v27tY .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-RL5tqbqnuy7v27tY .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-RL5tqbqnuy7v27tY .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-RL5tqbqnuy7v27tY .marker{fill:#333333;stroke:#333333;}#mermaid-svg-RL5tqbqnuy7v27tY .marker.cross{stroke:#333333;}#mermaid-svg-RL5tqbqnuy7v27tY svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-RL5tqbqnuy7v27tY p{margin:0;}#mermaid-svg-RL5tqbqnuy7v27tY .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-RL5tqbqnuy7v27tY .cluster-label text{fill:#333;}#mermaid-svg-RL5tqbqnuy7v27tY .cluster-label span{color:#333;}#mermaid-svg-RL5tqbqnuy7v27tY .cluster-label span p{background-color:transparent;}#mermaid-svg-RL5tqbqnuy7v27tY .label text,#mermaid-svg-RL5tqbqnuy7v27tY span{fill:#333;color:#333;}#mermaid-svg-RL5tqbqnuy7v27tY .node rect,#mermaid-svg-RL5tqbqnuy7v27tY .node circle,#mermaid-svg-RL5tqbqnuy7v27tY .node ellipse,#mermaid-svg-RL5tqbqnuy7v27tY .node polygon,#mermaid-svg-RL5tqbqnuy7v27tY .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-RL5tqbqnuy7v27tY .rough-node .label text,#mermaid-svg-RL5tqbqnuy7v27tY .node .label text,#mermaid-svg-RL5tqbqnuy7v27tY .image-shape .label,#mermaid-svg-RL5tqbqnuy7v27tY .icon-shape .label{text-anchor:middle;}#mermaid-svg-RL5tqbqnuy7v27tY .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-RL5tqbqnuy7v27tY .rough-node .label,#mermaid-svg-RL5tqbqnuy7v27tY .node .label,#mermaid-svg-RL5tqbqnuy7v27tY .image-shape .label,#mermaid-svg-RL5tqbqnuy7v27tY .icon-shape .label{text-align:center;}#mermaid-svg-RL5tqbqnuy7v27tY .node.clickable{cursor:pointer;}#mermaid-svg-RL5tqbqnuy7v27tY .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-RL5tqbqnuy7v27tY .arrowheadPath{fill:#333333;}#mermaid-svg-RL5tqbqnuy7v27tY .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-RL5tqbqnuy7v27tY .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-RL5tqbqnuy7v27tY .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-RL5tqbqnuy7v27tY .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-RL5tqbqnuy7v27tY .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-RL5tqbqnuy7v27tY .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-RL5tqbqnuy7v27tY .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-RL5tqbqnuy7v27tY .cluster text{fill:#333;}#mermaid-svg-RL5tqbqnuy7v27tY .cluster span{color:#333;}#mermaid-svg-RL5tqbqnuy7v27tY div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-RL5tqbqnuy7v27tY .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-RL5tqbqnuy7v27tY rect.text{fill:none;stroke-width:0;}#mermaid-svg-RL5tqbqnuy7v27tY .icon-shape,#mermaid-svg-RL5tqbqnuy7v27tY .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-RL5tqbqnuy7v27tY .icon-shape p,#mermaid-svg-RL5tqbqnuy7v27tY .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-RL5tqbqnuy7v27tY .icon-shape .label rect,#mermaid-svg-RL5tqbqnuy7v27tY .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-RL5tqbqnuy7v27tY .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-RL5tqbqnuy7v27tY .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-RL5tqbqnuy7v27tY :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Agent Request
Identity & Tenant
Policy Decision
Runtime Routing
Memory Recall
Token Budget
Tool / Workflow Execution
Event Capture
Memory Write-back
Audit & Retention

控制平面至少要回答这些问题:

  • 这个 Agent 代表谁?
  • 它属于哪个租户、团队和项目?
  • 它可以读取哪一类 Memory?
  • 它可以调用哪些 Tool?
  • 哪些动作需要人工审批?
  • 本次任务最多允许消耗多少 Token?
  • 任务完成后,哪些信息可以沉淀为长期经验?
  • 用户撤回授权后,哪些数据必须同步删除?

如果这些问题没有统一答案,所谓"企业级 Agent 平台"只是多个 Demo 的目录。


五、生产级核心代码:统一身份、权限与 Memory 作用域

下面这段代码不是一个完整的云平台,而是 Memory OS 控制平面的最小核心:用强类型对象描述租户、Agent 身份、Memory 作用域和读取策略,并在进入 Runtime 前完成统一授权。

python 复制代码
"""
memory_os_policy.py
Memory OS 控制平面的身份、作用域与策略判定核心。
Python 3.11+ / Pydantic v2
"""

from __future__ import annotations

from enum import Enum
from typing import FrozenSet

from pydantic import BaseModel, Field


class MemoryScope(str, Enum):
    USER = "user"
    TEAM = "team"
    PROJECT = "project"
    TENANT = "tenant"
    TASK = "task"


class Permission(str, Enum):
    READ = "memory.read"
    WRITE = "memory.write"
    DELETE = "memory.delete"
    PUBLISH_SKILL = "skill.publish"


class AgentIdentity(BaseModel):
    tenant_id: str = Field(min_length=1)
    subject_id: str = Field(min_length=1)
    agent_id: str = Field(min_length=1)
    team_ids: FrozenSet[str] = frozenset()
    project_ids: FrozenSet[str] = frozenset()
    task_ids: FrozenSet[str] = frozenset()
    permissions: FrozenSet[Permission] = frozenset()


class MemoryAddress(BaseModel):
    tenant_id: str
    scope: MemoryScope
    subject_id: str
    team_id: str | None = None
    project_id: str | None = None
    task_id: str | None = None


class MemoryPolicy(BaseModel):
    readable_scopes: FrozenSet[MemoryScope]
    writable_scopes: FrozenSet[MemoryScope]
    allow_delete: bool = False
    allow_publish_skill: bool = False


class PolicyDenied(PermissionError):
    pass


class MemoryPolicyEngine:
    """只负责策略判定,不直接访问数据库。"""

    def authorize_read(
        self,
        identity: AgentIdentity,
        address: MemoryAddress,
        policy: MemoryPolicy,
    ) -> None:
        self._check_common_access(identity, address, policy.readable_scopes)
        if Permission.READ not in identity.permissions:
            raise PolicyDenied("missing permission: memory.read")

    def authorize_write(
        self,
        identity: AgentIdentity,
        address: MemoryAddress,
        policy: MemoryPolicy,
    ) -> None:
        self._check_common_access(identity, address, policy.writable_scopes)
        if Permission.WRITE not in identity.permissions:
            raise PolicyDenied("missing permission: memory.write")

    def authorize_delete(
        self,
        identity: AgentIdentity,
        address: MemoryAddress,
        policy: MemoryPolicy,
    ) -> None:
        self._check_tenant(identity, address)
        if not policy.allow_delete or Permission.DELETE not in identity.permissions:
            raise PolicyDenied("memory deletion is not allowed")

    @staticmethod
    def _check_tenant(identity: AgentIdentity, address: MemoryAddress) -> None:
        if identity.tenant_id != address.tenant_id:
            raise PolicyDenied("cross-tenant memory access is forbidden")

    def _check_common_access(
        self,
        identity: AgentIdentity,
        address: MemoryAddress,
        allowed_scopes: FrozenSet[MemoryScope],
    ) -> None:
        self._check_tenant(identity, address)
        if address.scope not in allowed_scopes:
            raise PolicyDenied(f"scope={address.scope.value} is not allowed")

        if address.scope == MemoryScope.USER:
            if address.subject_id != identity.subject_id:
                raise PolicyDenied("cannot access another user's private memory")

        if address.scope == MemoryScope.TEAM:
            if not address.team_id or address.team_id not in identity.team_ids:
                raise PolicyDenied("agent is not a member of this team")

        if address.scope == MemoryScope.PROJECT:
            if not address.project_id or address.project_id not in identity.project_ids:
                raise PolicyDenied("agent is not a member of this project")

        if address.scope == MemoryScope.TASK:
            if not address.task_id or address.task_id not in identity.task_ids:
                raise PolicyDenied("agent is not a participant in this task")

这段代码有一个重要边界:Policy Engine 不应该直接读写数据库。

权限判断、Memory 查询、向量召回和事件写入分别属于控制平面与数据平面。把它们塞进一个"大而全"的 MemoryManager,短期代码少,长期会无法审计,也无法替换存储实现。


六、生产级核心代码:Provider 无关的 Memory Runtime 接口

Memory OS 不能绑定某一个向量数据库。Runtime 需要的是行为契约,而不是存储驱动。

下面代码与上一段代码属于同一个最小工程:

text 复制代码
memory-os-runtime/
├── memory_os_policy.py
├── memory_runtime.py
└── test_memory_runtime.py

memory_runtime.py

python 复制代码
"""
memory_runtime.py
Provider 无关的 Memory Runtime 核心接口。
"""

from __future__ import annotations

from datetime import datetime, timezone
from typing import Any, Protocol, Sequence
from uuid import UUID, uuid4

from pydantic import BaseModel, Field

from memory_os_policy import (
    AgentIdentity,
    MemoryAddress,
    MemoryPolicy,
    MemoryPolicyEngine,
)


class MemoryRecord(BaseModel):
    id: UUID = Field(default_factory=uuid4)
    address: MemoryAddress
    content: str = Field(min_length=1)
    category: str
    confidence: float = Field(ge=0.0, le=1.0)
    importance: float = Field(ge=0.0, le=1.0)
    version: int = Field(default=1, ge=1)
    created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
    metadata: dict[str, Any] = Field(default_factory=dict)


class RecallQuery(BaseModel):
    address: MemoryAddress
    query: str = Field(min_length=1)
    top_k: int = Field(default=8, ge=1, le=50)
    token_budget: int = Field(default=800, ge=100, le=8000)


class MemoryStore(Protocol):
    async def recall(self, query: RecallQuery) -> Sequence[MemoryRecord]: ...
    async def append(self, record: MemoryRecord) -> MemoryRecord: ...
    async def compare_and_swap(
        self,
        record_id: UUID,
        expected_version: int,
        content: str,
    ) -> MemoryRecord: ...


class InMemoryStore:
    """教学用内存 Provider,用于本地测试 Runtime,不代表生产存储。"""

    def __init__(self) -> None:
        self.records: dict[UUID, MemoryRecord] = {}

    async def recall(self, query: RecallQuery) -> Sequence[MemoryRecord]:
        candidates = [
            item
            for item in self.records.values()
            if item.address.tenant_id == query.address.tenant_id
            and item.address.scope == query.address.scope
            and item.address.subject_id == query.address.subject_id
        ]
        return candidates[: query.top_k]

    async def append(self, record: MemoryRecord) -> MemoryRecord:
        self.records[record.id] = record
        return record

    async def compare_and_swap(
        self,
        record_id: UUID,
        expected_version: int,
        content: str,
    ) -> MemoryRecord:
        current = self.records[record_id]
        if current.version != expected_version:
            raise RuntimeError(
                f"version conflict: expected={expected_version}, "
                f"actual={current.version}"
            )
        updated = current.model_copy(
            update={
                "content": content,
                "version": current.version + 1,
                "updated_at": datetime.now(timezone.utc),
            }
        )
        self.records[record_id] = updated
        return updated


class MemoryRuntime:
    """策略判定、召回预算和 Provider 存储之间的编排层。"""

    def __init__(
        self,
        store: MemoryStore,
        policy_engine: MemoryPolicyEngine,
    ) -> None:
        self.store = store
        self.policy_engine = policy_engine

    async def recall(
        self,
        identity: AgentIdentity,
        policy: MemoryPolicy,
        query: RecallQuery,
    ) -> list[MemoryRecord]:
        self.policy_engine.authorize_read(identity, query.address, policy)
        records = await self.store.recall(query)
        return self._fit_budget(records, query.token_budget)

    async def write_back(
        self,
        identity: AgentIdentity,
        policy: MemoryPolicy,
        record: MemoryRecord,
    ) -> MemoryRecord:
        self.policy_engine.authorize_write(identity, record.address, policy)
        if record.address.tenant_id != identity.tenant_id:
            raise PermissionError("tenant mismatch")
        return await self.store.append(record)

    @staticmethod
    def _fit_budget(
        records: Sequence[MemoryRecord],
        token_budget: int,
    ) -> list[MemoryRecord]:
        selected: list[MemoryRecord] = []
        used = 0
        for record in sorted(
            records,
            key=lambda item: (item.importance, item.confidence),
            reverse=True,
        ):
            estimated_tokens = max(1, len(record.content) // 2)
            if used + estimated_tokens > token_budget:
                continue
            selected.append(record)
            used += estimated_tokens
        return selected

test_memory_runtime.py

python 复制代码
import asyncio

from memory_os_policy import (
    AgentIdentity,
    MemoryAddress,
    MemoryPolicy,
    MemoryPolicyEngine,
    MemoryScope,
    Permission,
    PolicyDenied,
)
from memory_runtime import InMemoryStore, MemoryRecord, MemoryRuntime, RecallQuery


async def main() -> None:
    identity = AgentIdentity(
        tenant_id="tenant-a",
        subject_id="user-1",
        agent_id="coding-agent",
        team_ids=frozenset({"team-platform"}),
        project_ids=frozenset({"project-platform"}),
        task_ids=frozenset({"task-001"}),
        permissions=frozenset({Permission.READ, Permission.WRITE}),
    )
    policy = MemoryPolicy(
        readable_scopes=frozenset({MemoryScope.USER, MemoryScope.PROJECT}),
        writable_scopes=frozenset({MemoryScope.USER, MemoryScope.PROJECT}),
    )
    address = MemoryAddress(
        tenant_id="tenant-a",
        scope=MemoryScope.PROJECT,
        subject_id="user-1",
        project_id="project-platform",
    )
    store = InMemoryStore()
    runtime = MemoryRuntime(store, MemoryPolicyEngine())

    record = MemoryRecord(
        address=address,
        content="项目使用 Python 3.11,测试命令是 pytest -q。",
        category="procedural",
        confidence=0.98,
        importance=0.9,
    )
    await runtime.write_back(identity, policy, record)
    result = await runtime.recall(
        identity,
        policy,
        RecallQuery(address=address, query="项目测试命令"),
    )
    assert len(result) == 1
    assert result[0].content.startswith("项目使用 Python")

    other_user_address = address.model_copy(update={"subject_id": "user-2"})
    try:
        await runtime.recall(
            identity,
            policy,
            RecallQuery(address=other_user_address, query="越权读取"),
        )
    except PolicyDenied:
        pass
    else:
        raise AssertionError("cross-user access should be denied")

    print("Memory Runtime verification PASS")


if __name__ == "__main__":
    asyncio.run(main())

运行方式:

bash 复制代码
python -m pip install "pydantic>=2,<3"
python test_memory_runtime.py

这组代码形成了一个可以实际运行的最小闭环:

text 复制代码
AgentIdentity
    ↓
MemoryPolicy
    ↓
PolicyEngine
    ↓
MemoryRuntime
    ↓
InMemoryStore
    ↓
Recall / Write-back / 越权拒绝

这个 InMemoryStore 只用于教学和单元测试。生产环境可以替换成 PostgreSQL、pgvector、Qdrant、Neo4j 或远程 Memory Service,而不需要修改 Runtime 的策略编排接口。

一句话总结这一章的核心观点:

Provider 负责保存数据,Memory Runtime 负责执行策略、召回和写回;两者解耦,平台才有演进空间。


七、跨 Agent 共享记忆:共享不是全量开放

企业共享 Memory,不能理解成所有 Agent 读取同一张表。

正确的共享模型应该是"按主体、团队、项目和任务分层授权":
#mermaid-svg-88LItNEbz0VRSVlA{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-88LItNEbz0VRSVlA .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-88LItNEbz0VRSVlA .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-88LItNEbz0VRSVlA .error-icon{fill:#552222;}#mermaid-svg-88LItNEbz0VRSVlA .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-88LItNEbz0VRSVlA .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-88LItNEbz0VRSVlA .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-88LItNEbz0VRSVlA .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-88LItNEbz0VRSVlA .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-88LItNEbz0VRSVlA .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-88LItNEbz0VRSVlA .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-88LItNEbz0VRSVlA .marker{fill:#333333;stroke:#333333;}#mermaid-svg-88LItNEbz0VRSVlA .marker.cross{stroke:#333333;}#mermaid-svg-88LItNEbz0VRSVlA svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-88LItNEbz0VRSVlA p{margin:0;}#mermaid-svg-88LItNEbz0VRSVlA .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-88LItNEbz0VRSVlA .cluster-label text{fill:#333;}#mermaid-svg-88LItNEbz0VRSVlA .cluster-label span{color:#333;}#mermaid-svg-88LItNEbz0VRSVlA .cluster-label span p{background-color:transparent;}#mermaid-svg-88LItNEbz0VRSVlA .label text,#mermaid-svg-88LItNEbz0VRSVlA span{fill:#333;color:#333;}#mermaid-svg-88LItNEbz0VRSVlA .node rect,#mermaid-svg-88LItNEbz0VRSVlA .node circle,#mermaid-svg-88LItNEbz0VRSVlA .node ellipse,#mermaid-svg-88LItNEbz0VRSVlA .node polygon,#mermaid-svg-88LItNEbz0VRSVlA .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-88LItNEbz0VRSVlA .rough-node .label text,#mermaid-svg-88LItNEbz0VRSVlA .node .label text,#mermaid-svg-88LItNEbz0VRSVlA .image-shape .label,#mermaid-svg-88LItNEbz0VRSVlA .icon-shape .label{text-anchor:middle;}#mermaid-svg-88LItNEbz0VRSVlA .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-88LItNEbz0VRSVlA .rough-node .label,#mermaid-svg-88LItNEbz0VRSVlA .node .label,#mermaid-svg-88LItNEbz0VRSVlA .image-shape .label,#mermaid-svg-88LItNEbz0VRSVlA .icon-shape .label{text-align:center;}#mermaid-svg-88LItNEbz0VRSVlA .node.clickable{cursor:pointer;}#mermaid-svg-88LItNEbz0VRSVlA .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-88LItNEbz0VRSVlA .arrowheadPath{fill:#333333;}#mermaid-svg-88LItNEbz0VRSVlA .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-88LItNEbz0VRSVlA .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-88LItNEbz0VRSVlA .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-88LItNEbz0VRSVlA .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-88LItNEbz0VRSVlA .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-88LItNEbz0VRSVlA .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-88LItNEbz0VRSVlA .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-88LItNEbz0VRSVlA .cluster text{fill:#333;}#mermaid-svg-88LItNEbz0VRSVlA .cluster span{color:#333;}#mermaid-svg-88LItNEbz0VRSVlA div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-88LItNEbz0VRSVlA .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-88LItNEbz0VRSVlA rect.text{fill:none;stroke-width:0;}#mermaid-svg-88LItNEbz0VRSVlA .icon-shape,#mermaid-svg-88LItNEbz0VRSVlA .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-88LItNEbz0VRSVlA .icon-shape p,#mermaid-svg-88LItNEbz0VRSVlA .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-88LItNEbz0VRSVlA .icon-shape .label rect,#mermaid-svg-88LItNEbz0VRSVlA .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-88LItNEbz0VRSVlA .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-88LItNEbz0VRSVlA .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-88LItNEbz0VRSVlA :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户私有记忆
用户授权的 Agent
团队共享记忆
团队内 Agent
项目记忆
项目参与 Agent
组织公共规范
租户内 Agent
Memory Gateway
Scope Policy + RBAC + ABAC
Memory Runtime

"可共享"与"默认可读"不是一回事。

  • 🔸 用户私有记忆默认只对该用户授权的 Agent 可见;
  • 🔸 团队记忆必须绑定团队成员关系;
  • 🔸 项目记忆必须绑定项目角色与任务上下文;
  • 🔸 组织规范可以共享,但凭据、个人信息和客户敏感数据不能进入公共作用域。

这也是 Memory OS 与普通 RAG 知识库的区别:RAG 主要解决资料分发,Memory OS 还要解决主体归属、状态变化和权限撤回。

一句话总结这一章的核心观点:

共享 Memory 的前提不是"大家都能读",而是每次读取都能证明"谁在什么作用域内读取什么"。


八、企业 Memory OS 的演进路线

企业不需要一开始就部署完整的 Memory OS。更稳妥的演进路线是:

text 复制代码
阶段 1:Agent 内置 Memory
        └── 单 Agent、单租户、SQLite / 文件存储

阶段 2:团队级 Memory Service
        └── 统一 Recall / Write-back / Provider 接口
        └── 增加身份、权限、审计与 Token 配额

阶段 3:企业级 Memory Platform
        └── 多 Agent 共享、黑板协作、异步 Reflection
        └── PostgreSQL + Vector + Graph + Object Storage

阶段 4:Memory OS
        └── Memory Runtime 成为 Agent Runtime 的基础平面
        └── 组织经验、Skill、任务状态和审计事件统一治理

不要为了"平台化"而平台化。

当企业只有一个 Agent、没有跨会话状态、没有多租户隔离诉求时,一个嵌入式 Memory Provider 足够;当多个 Agent 开始共享用户、项目和组织经验时,Memory Service 才具有独立存在的价值;当平台开始承担权限、审计、成本、Skill 和工作流治理时,它才真正接近 Memory OS。


九、与最终实战篇的衔接

到这里,我们已经完成了企业级 Agent 的平台架构:

  • 🔸 用 Memory Runtime 管理跨会话状态;
  • 🔸 用 MCP Gateway 管理外部工具;
  • 🔸 用 Workflow Runtime 管理 DAG、FSM 和 Subagent;
  • 🔸 用 Evaluation 与 Guardrails 管理质量和安全;
  • 🔸 用 Memory OS 把多个 Agent 的经验沉淀为企业资产。

下一篇不再继续堆平台组件,而是反过来拆解这个系统最小的可工作内核:

不用重量级 Agent 框架,只用约 300 行 Python 代码,完成一个能够读取文件、修改代码、运行测试并根据真实错误继续修复的 Coding Agent。

这不是为了证明 300 行代码能够替代企业平台,而是为了让我们看清:所有复杂 Agent Runtime,最终都建立在一个清晰的 Conversation Loop、确定性 Tool 和真实执行反馈之上。


本篇总结

  • 🔸 企业需要统一治理 Agent 身份、工具、Memory、Workflow、预算和审计;
  • 🔸 Memory OS 管理的不是聊天记录,而是主体认知、任务状态、程序性技能和事件证据;
  • 🔸 Memory Runtime 是控制平面的核心,Provider 和数据库属于可替换的数据平面;
  • 🔸 共享 Memory 必须按 User、Team、Project、Tenant 分层授权,不能全量开放;
  • 🔸 企业应从嵌入式 Memory 开始,按真实的跨 Agent 协作需求逐步演进到 Memory OS。

终局不是部署更多 Agent,而是让所有 Agent 共享一套可控、可审计、可持续演化的企业心智。

下一篇:《终章实战|300 行纯 Python 代码,手搓一个 Coding Agent》


十、把前面所有章节串成一个完整项目

前面的章节并不是一组互不相关的代码片段。

它们共同描述了一条完整的企业 Agent 请求链路:
#mermaid-svg-1RbiwKyx6a0AxCgC{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-1RbiwKyx6a0AxCgC .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-1RbiwKyx6a0AxCgC .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-1RbiwKyx6a0AxCgC .error-icon{fill:#552222;}#mermaid-svg-1RbiwKyx6a0AxCgC .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-1RbiwKyx6a0AxCgC .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-1RbiwKyx6a0AxCgC .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-1RbiwKyx6a0AxCgC .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-1RbiwKyx6a0AxCgC .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-1RbiwKyx6a0AxCgC .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-1RbiwKyx6a0AxCgC .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-1RbiwKyx6a0AxCgC .marker{fill:#333333;stroke:#333333;}#mermaid-svg-1RbiwKyx6a0AxCgC .marker.cross{stroke:#333333;}#mermaid-svg-1RbiwKyx6a0AxCgC svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-1RbiwKyx6a0AxCgC p{margin:0;}#mermaid-svg-1RbiwKyx6a0AxCgC .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-1RbiwKyx6a0AxCgC .cluster-label text{fill:#333;}#mermaid-svg-1RbiwKyx6a0AxCgC .cluster-label span{color:#333;}#mermaid-svg-1RbiwKyx6a0AxCgC .cluster-label span p{background-color:transparent;}#mermaid-svg-1RbiwKyx6a0AxCgC .label text,#mermaid-svg-1RbiwKyx6a0AxCgC span{fill:#333;color:#333;}#mermaid-svg-1RbiwKyx6a0AxCgC .node rect,#mermaid-svg-1RbiwKyx6a0AxCgC .node circle,#mermaid-svg-1RbiwKyx6a0AxCgC .node ellipse,#mermaid-svg-1RbiwKyx6a0AxCgC .node polygon,#mermaid-svg-1RbiwKyx6a0AxCgC .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-1RbiwKyx6a0AxCgC .rough-node .label text,#mermaid-svg-1RbiwKyx6a0AxCgC .node .label text,#mermaid-svg-1RbiwKyx6a0AxCgC .image-shape .label,#mermaid-svg-1RbiwKyx6a0AxCgC .icon-shape .label{text-anchor:middle;}#mermaid-svg-1RbiwKyx6a0AxCgC .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-1RbiwKyx6a0AxCgC .rough-node .label,#mermaid-svg-1RbiwKyx6a0AxCgC .node .label,#mermaid-svg-1RbiwKyx6a0AxCgC .image-shape .label,#mermaid-svg-1RbiwKyx6a0AxCgC .icon-shape .label{text-align:center;}#mermaid-svg-1RbiwKyx6a0AxCgC .node.clickable{cursor:pointer;}#mermaid-svg-1RbiwKyx6a0AxCgC .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-1RbiwKyx6a0AxCgC .arrowheadPath{fill:#333333;}#mermaid-svg-1RbiwKyx6a0AxCgC .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-1RbiwKyx6a0AxCgC .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-1RbiwKyx6a0AxCgC .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1RbiwKyx6a0AxCgC .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-1RbiwKyx6a0AxCgC .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1RbiwKyx6a0AxCgC .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-1RbiwKyx6a0AxCgC .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-1RbiwKyx6a0AxCgC .cluster text{fill:#333;}#mermaid-svg-1RbiwKyx6a0AxCgC .cluster span{color:#333;}#mermaid-svg-1RbiwKyx6a0AxCgC div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-1RbiwKyx6a0AxCgC .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-1RbiwKyx6a0AxCgC rect.text{fill:none;stroke-width:0;}#mermaid-svg-1RbiwKyx6a0AxCgC .icon-shape,#mermaid-svg-1RbiwKyx6a0AxCgC .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1RbiwKyx6a0AxCgC .icon-shape p,#mermaid-svg-1RbiwKyx6a0AxCgC .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-1RbiwKyx6a0AxCgC .icon-shape .label rect,#mermaid-svg-1RbiwKyx6a0AxCgC .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1RbiwKyx6a0AxCgC .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-1RbiwKyx6a0AxCgC .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-1RbiwKyx6a0AxCgC :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} User Request
Guardrails

输入清洗 / 越狱检测
Agent Gateway

Identity / Tenant / Budget
Memory Runtime

Scope Policy / Recall
Agent Runtime

Plan / Reason / Decide
Tool Plane

Registry / MCP / Sandbox
Workflow Runtime

DAG / FSM / Blackboard
Observability

Trace / Token / Cost
Evaluation

Trajectory / State / Score
Memory Write-back

Reflection / Consolidation
Response

如果只看单篇文章,读者会得到一个 Memory Provider、一个 MCP Server、一个 DAG、一个 Guardrail 或一个评测器;把它们放进同一条链路之后,才会得到一个真正的 Agent Platform。

10.1 项目目录

本文对应的可运行参考项目已经落盘到:

text 复制代码
G:/tmp/enterprise-agent-platform/
├── pyproject.toml
├── README.md
├── src/
│   └── eap/
│       ├── __init__.py
│       ├── policy.py       # Tenant / User / Team / Project / Task 权限
│       ├── memory.py       # MemoryStore / Recall / Write-back / CAS
│       ├── tools.py        # Tool Registry / 风险授权 / 确定性工具
│       ├── workflow.py     # DAG 依赖执行与死循环保护
│       ├── quality.py      # Guardrails / Trace / Evaluation
│       └── gateway.py      # 统一 Agent Gateway
└── tests/
    └── test_integration.py

这不是为了再造一个大型框架,而是把本系列的核心判断压缩成一个可以启动、测试和替换 Provider 的最小控制平面。

10.2 每个专栏在项目中的位置

专栏 文章解决的问题 项目模块 进入请求链路的位置
Agent Memory Agent 如何保持跨会话连续性 policy.pymemory.py Recall / Write-back
MCP 与 Tools Agent 如何安全连接外部世界 tools.py Tool Plane
Multi-Agent 多个执行单元如何协作 workflow.py Workflow Plane
Evaluation 与 Security 如何阻止、观测和评估 Agent quality.py Gateway 前后与执行中
Enterprise Agent 如何把这些模块统一治理 gateway.py 全链路编排
300 行 Coding Agent Agent 最小执行内核是什么 Tool Contract / Conversation Loop Runtime 内部

有一个边界必须讲清楚:项目中的 InMemoryStore、确定性 Tool Handler 和本地 Workflow 是测试替身,不是生产基础设施。它们的价值在于固定控制流,让权限、失败和终态可以被测试;接入 PostgreSQL、Qdrant、MCP Server、GitLab 或真实模型时,应该替换 Provider,而不是重写 Gateway。

10.3 统一入口如何串联模块

AgentGateway.run() 是整个项目的入口。它不负责替模型思考,也不把所有业务逻辑塞进一个函数,而是按照治理顺序编排模块:

python 复制代码
async def run(self, identity, policy, request):
    incoming = guardrails.sanitize_input(request)
    memories = await memory.recall(identity, policy, query)
    tool_result = tools.execute(identity, "read_file", arguments)
    workflow_result = workflow.run({
        "request": request,
        "memory_count": len(memories),
        "tool": tool_result,
    })
    output = guardrails.verify_output(str(workflow_result))
    trace.add("agent.run", token_usage)
    return output

这段代码看起来很短,但它对应了前面所有文章的工程边界:

  • 🔸 Guardrails 在模型和工具之前拦截不可信输入;
  • 🔸 Policy Engine 在 Memory 查询之前拦截越租户、越用户和越项目访问;
  • 🔸 Memory Runtime 只暴露 Recall 和 Write-back,不把数据库暴露给 Agent;
  • 🔸 Tool Registry 只允许注册过且通过权限检查的工具;
  • 🔸 Workflow Runtime 处理依赖关系,不让 Agent 通过自然语言随意形成调用风暴;
  • 🔸 Trace 记录本次执行的 Token、工具和状态;
  • 🔸 Evaluation 判断这次执行是否真的完成,而不是只判断模型是否返回了文本。

10.4 从用户请求到最终结果的完整时序

Evaluation Workflow Runtime Tool Registry Memory Runtime Policy Engine Agent Gateway User Evaluation Workflow Runtime Tool Registry Memory Runtime Policy Engine Agent Gateway User #mermaid-svg-6hHmR7pZlv27xxVN{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-6hHmR7pZlv27xxVN .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-6hHmR7pZlv27xxVN .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-6hHmR7pZlv27xxVN .error-icon{fill:#552222;}#mermaid-svg-6hHmR7pZlv27xxVN .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-6hHmR7pZlv27xxVN .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-6hHmR7pZlv27xxVN .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-6hHmR7pZlv27xxVN .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-6hHmR7pZlv27xxVN .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-6hHmR7pZlv27xxVN .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-6hHmR7pZlv27xxVN .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-6hHmR7pZlv27xxVN .marker{fill:#333333;stroke:#333333;}#mermaid-svg-6hHmR7pZlv27xxVN .marker.cross{stroke:#333333;}#mermaid-svg-6hHmR7pZlv27xxVN svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-6hHmR7pZlv27xxVN p{margin:0;}#mermaid-svg-6hHmR7pZlv27xxVN .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-6hHmR7pZlv27xxVN text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-6hHmR7pZlv27xxVN .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-6hHmR7pZlv27xxVN .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-6hHmR7pZlv27xxVN .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-6hHmR7pZlv27xxVN .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-6hHmR7pZlv27xxVN #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-6hHmR7pZlv27xxVN .sequenceNumber{fill:white;}#mermaid-svg-6hHmR7pZlv27xxVN #sequencenumber{fill:#333;}#mermaid-svg-6hHmR7pZlv27xxVN #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-6hHmR7pZlv27xxVN .messageText{fill:#333;stroke:none;}#mermaid-svg-6hHmR7pZlv27xxVN .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-6hHmR7pZlv27xxVN .labelText,#mermaid-svg-6hHmR7pZlv27xxVN .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-6hHmR7pZlv27xxVN .loopText,#mermaid-svg-6hHmR7pZlv27xxVN .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-6hHmR7pZlv27xxVN .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-6hHmR7pZlv27xxVN .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-6hHmR7pZlv27xxVN .noteText,#mermaid-svg-6hHmR7pZlv27xxVN .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-6hHmR7pZlv27xxVN .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-6hHmR7pZlv27xxVN .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-6hHmR7pZlv27xxVN .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-6hHmR7pZlv27xxVN .actorPopupMenu{position:absolute;}#mermaid-svg-6hHmR7pZlv27xxVN .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-6hHmR7pZlv27xxVN .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-6hHmR7pZlv27xxVN .actor-man circle,#mermaid-svg-6hHmR7pZlv27xxVN line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-6hHmR7pZlv27xxVN :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} requestsanitize inputauthorize tenant / scopeallowrecall scoped memoriesranked memoriesexecute registered tooltool resultrun dependency graphworkflow stateevaluate trajectory and terminal statepass / failwrite-back verified resultresponse + trace id

这个顺序不是形式主义。把 Write-back 放在执行前,会把模型猜测写成事实;把权限检查放在 Recall 后面,会产生数据泄露;把 Evaluation 放在最终响应之后,会导致错误结果已经进入下游系统。

10.5 项目启动与真实验收

项目使用 Python 3.11 和 Pydantic v2,不依赖外部模型、数据库或 MCP 服务,可以在本地先验证控制平面。

bash 复制代码
cd G:/tmp/enterprise-agent-platform
python -m pip install -e .
python tests/test_integration.py

真实验收覆盖以下链路:

text 复制代码
1. 创建 tenant-a / user-1 / coding-agent 身份
2. 授予 Memory Read、Memory Write、Tool Execute 权限
3. 写入一条用户私有 Memory
4. 通过 Gateway Recall Memory
5. 注册 read_file 与 run_tests Tool
6. 执行 plan -> verify 工作流
7. 生成 Trace 与 Token 统计
8. Evaluation 返回 score=1.0
9. 输入越狱文本被 Guardrails 拦截
10. user-2 读取 user-1 Memory 被 PolicyDenied 拒绝

验证输出:

text 复制代码
Enterprise Agent Platform integration PASS
Evaluation PASS {'passed': True, 'score': 1.0, 'reasons': []}

这次验收验证的是模块之间真实连接,而不是分别执行几个孤立类的 main()。它证明了:Memory、Tool、Workflow、Guardrails、Evaluation 和 Gateway 已经形成一条可执行链路。

10.6 哪些代码应该替换成生产实现?

这个参考项目刻意把"控制契约"和"基础设施实现"分开。

当前本地实现 生产替换方案 不应该改变的契约
InMemoryStore PostgreSQL + pgvector / Qdrant / Neo4j recallappendcompare_and_swap
deterministic_read MCP Resource / 文件服务 Tool 输入输出与审计事件
deterministic_test 沙箱、CI Runner、Kubernetes Job exit_code、stdout、timeout
本地 WorkflowRuntime Temporal、队列或分布式 DAG Step、依赖、状态和幂等语义
本地 Trace OpenTelemetry Collector trace_id、span、token、cost
本地 Guardrails Policy Service、DLP、模型分类器 allow / block / sanitized result
本地 evaluate Golden Dataset、离线评测平台 score、reasons、terminal state

如果替换 Provider 的同时修改了 Gateway 的业务语义,说明系统没有真正解耦;如果所有实现都必须依赖某一家数据库或某个 Agent 框架,说明抽象层还停留在 SDK 包装阶段。

10.7 这套项目还缺什么?

它已经是一个可运行的控制平面,但不是可以直接承载生产流量的完整产品。进入生产前还要补齐:

  • 🔸 真正的 OAuth2 / JWT 验签和密钥轮换;
  • 🔸 PostgreSQL 事务、唯一约束、审计事件和数据删除;
  • 🔸 向量召回、关键词召回和多路排序;
  • 🔸 MCP Streamable HTTP 会话、心跳和连接淘汰;
  • 🔸 gVisor、Firecracker 或受限容器沙箱;
  • 🔸 分布式 Workflow 的幂等、重试和补偿;
  • 🔸 OpenTelemetry Exporter 与成本账单;
  • 🔸 Golden Dataset、回归集和人工复核队列;
  • 🔸 多租户限流、熔断、密钥隔离和合规删除。

这些不是本文没有想到,而是有意把"可运行的最小闭环"和"生产级基础设施"分成两层。架构文章最危险的写法,是用一个内存字典冒充企业平台;同样危险的是,一上来堆满 Kafka、Kubernetes、向量数据库,却没有一条可以跑通的请求链路。

一句话总结这一章的核心观点:

前面所有章节的价值,不在于分别实现几个组件,而在于把它们串成一条可授权、可执行、可观测、可评估、可沉淀的 Agent 请求链路。


本篇最终总结

  • 🔸 Agent Platform 管理执行,Memory OS 管理连续性;
  • 🔸 多个 Agent 开始共同读写业务状态时,平台化才从选择变成基础设施;
  • 🔸 Memory 必须按 User、Team、Project、Tenant 和 Task 作用域授权;
  • 🔸 Tool、Workflow、Memory、Guardrails 和 Evaluation 不能各自维护一套身份与审计;
  • 🔸 Provider 可以替换,核心契约和控制流不能随着存储实现漂移;
  • 🔸 最小可运行项目比一张宏大的架构图更能证明设计是否成立;
  • 🔸 300 行 Coding Agent 是完整平台的最小执行内核,不是企业平台的替代品。

企业 Agent 的终点不是"让模型更会聊天",而是让一组 Agent 在统一身份、权限、工具、Memory、Workflow 和评测体系下持续工作。


十一、真正可使用的 CLI:从控制平面 Demo 到 Coding Agent

前面的 Gateway 集成测试证明了模块可以连接,但它还不能替用户完成真实代码工作。真正的使用入口应该是一个类似 Claude Code 的命令行:用户输入任务,模型决定是否调用工具,工具结果再回到模型,直到模型给出最终答案。

本项目现在已经补上这条真实闭环,入口位于:

text 复制代码
G:/tmp/enterprise-agent-platform/src/eap/cli.py

启动方式:

bash 复制代码
cd G:/tmp/enterprise-agent-platform
python -m pip install -e .

export EAP_API_KEY='你的模型 API Key'
export EAP_BASE_URL='https://www.xxxx.xx/v1'
export EAP_MODEL='GLM-5.2-CL'

PYTHONPATH=src python -m eap.cli --cwd G:/tmp/your-repo

Windows PowerShell:

powershell 复制代码
cd G:\tmp\enterprise-agent-platform
python -m pip install -e .

$env:EAP_API_KEY = "你的模型 API Key"
$env:EAP_BASE_URL = "https://www.xxxx.xx/v1"
$env:EAP_MODEL = "GLM-5.2-CL"

python -m eap.cli --cwd G:\tmp\your-repo

CLI 支持四个内置命令:

命令 作用
/help 查看帮助
/status 查看模型、工作目录和工具
/clear 清空当前对话上下文
/quit 退出 CLI

真实 Agent Loop 如下:
Coding Tools Guardrails + Memory OpenAI-compatible LLM CLI User Coding Tools Guardrails + Memory OpenAI-compatible LLM CLI User #mermaid-svg-V5jdvEa2J8rqJD4o{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-V5jdvEa2J8rqJD4o .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-V5jdvEa2J8rqJD4o .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-V5jdvEa2J8rqJD4o .error-icon{fill:#552222;}#mermaid-svg-V5jdvEa2J8rqJD4o .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-V5jdvEa2J8rqJD4o .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-V5jdvEa2J8rqJD4o .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-V5jdvEa2J8rqJD4o .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-V5jdvEa2J8rqJD4o .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-V5jdvEa2J8rqJD4o .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-V5jdvEa2J8rqJD4o .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-V5jdvEa2J8rqJD4o .marker{fill:#333333;stroke:#333333;}#mermaid-svg-V5jdvEa2J8rqJD4o .marker.cross{stroke:#333333;}#mermaid-svg-V5jdvEa2J8rqJD4o svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-V5jdvEa2J8rqJD4o p{margin:0;}#mermaid-svg-V5jdvEa2J8rqJD4o .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-V5jdvEa2J8rqJD4o text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-V5jdvEa2J8rqJD4o .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-V5jdvEa2J8rqJD4o .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-V5jdvEa2J8rqJD4o .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-V5jdvEa2J8rqJD4o .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-V5jdvEa2J8rqJD4o #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-V5jdvEa2J8rqJD4o .sequenceNumber{fill:white;}#mermaid-svg-V5jdvEa2J8rqJD4o #sequencenumber{fill:#333;}#mermaid-svg-V5jdvEa2J8rqJD4o #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-V5jdvEa2J8rqJD4o .messageText{fill:#333;stroke:none;}#mermaid-svg-V5jdvEa2J8rqJD4o .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-V5jdvEa2J8rqJD4o .labelText,#mermaid-svg-V5jdvEa2J8rqJD4o .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-V5jdvEa2J8rqJD4o .loopText,#mermaid-svg-V5jdvEa2J8rqJD4o .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-V5jdvEa2J8rqJD4o .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-V5jdvEa2J8rqJD4o .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-V5jdvEa2J8rqJD4o .noteText,#mermaid-svg-V5jdvEa2J8rqJD4o .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-V5jdvEa2J8rqJD4o .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-V5jdvEa2J8rqJD4o .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-V5jdvEa2J8rqJD4o .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-V5jdvEa2J8rqJD4o .actorPopupMenu{position:absolute;}#mermaid-svg-V5jdvEa2J8rqJD4o .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-V5jdvEa2J8rqJD4o .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-V5jdvEa2J8rqJD4o .actor-man circle,#mermaid-svg-V5jdvEa2J8rqJD4o line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-V5jdvEa2J8rqJD4o :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 输入任务输入安全检查与 Memory Recallmessages + tool schemasassistant / tool_callsread_file / search_files / patch_file / run_commandtool resulttool result + conversation history下一轮 tool_call 或最终回答输出检查与 Conversation Memory write-back最终结果

可用的真实工具包括:

  • 🔸 read_file:读取工作目录内的 UTF-8 文本文件;
  • 🔸 search_files:按文件名搜索项目;
  • 🔸 patch_file:要求旧文本唯一匹配后执行精准替换;
  • 🔸 run_command:在工作目录执行测试或检查命令,需要用户确认。

这几个工具对应前面 Coding Agent 文章中的最小确定性工具集,但现在不再由固定代码自动调用,而是由真实模型通过 Tool Calling 决定调用顺序。

CLI 的安全边界

text 复制代码
工作目录 root
    ├── read_file 只能读取 root 内文件
    ├── search_files 只能搜索 root 内文件
    ├── patch_file 只能修改 root 内文件
    └── run_command 只能在 root 内执行,并且需要人工确认

patch_file 不允许整文件覆盖,只接受 oldnew 两个片段,并且要求 old 在文件中只出现一次。run_commandpatch_file 都会在 CLI 中询问:

text 复制代码
Approve patch_file({...})? [y/N]
Approve run_command({...})? [y/N]

这还不是 gVisor 或 Firecracker 级别的生产沙箱,但已经比直接把 shell=True 暴露给模型多了一层工作目录边界、用户审批和输出截断。

真实使用方式

进入 CLI 后,可以这样使用:

text 复制代码
> 读取 README.md,解释这个项目的结构,不要修改文件。

> 找出失败的测试,定位根因,只做最小 Patch,然后运行测试。

> 检查最近修改的 Python 文件,先给我风险分析,不要执行命令。

当模型返回 read_filesearch_files 时,CLI 会自动执行并把结果回传给模型;当模型尝试 patch_filerun_command 时,CLI 会暂停等待人工批准。这个交互顺序才是 Claude Code 风格 Coding Agent 的基本形态。


十二、全部真实代码:CLI 三大核心模块

这一章给出 CLI 的全部真实源码。这些代码不是文章示意,而是上一节那条实测会话正在执行的代码,可以直接复制运行。

12.1 llm.py:OpenAI 兼容模型适配层

整个模型层只有一个类:从环境变量读配置,向 /chat/completions 发送带 tools 的请求,返回 messageusage。没有任何 Provider 魔法,换模型只改环境变量。

python 复制代码
from __future__ import annotations

import json
import os
from typing import Any

import httpx


class LLMError(RuntimeError):
    pass


class OpenAICompatibleLLM:
    def __init__(self, *, api_key: str, base_url: str, model: str, timeout: float = 180.0) -> None:
        self.api_key = api_key
        self.base_url = base_url.rstrip("/")
        self.model = model
        self.timeout = timeout

    @classmethod
    def from_env(cls) -> "OpenAICompatibleLLM":
        api_key = os.getenv("EAP_API_KEY", "").strip()
        if not api_key:
            raise LLMError("EAP_API_KEY is not set")
        return cls(
            api_key=api_key,
            base_url=os.getenv("EAP_BASE_URL", "https://www.xxxx.xx/v1"),
            model=os.getenv("EAP_MODEL", "GLM-5.2-CL"),
        )

    async def complete(self, messages: list[dict[str, Any]], tools: list[dict[str, Any]]) -> dict[str, Any]:
        payload = {
            "model": self.model,
            "messages": messages,
            "tools": tools,
            "tool_choice": "auto",
            "temperature": 0,
        }
        url = f"{self.base_url}/chat/completions"
        try:
            async with httpx.AsyncClient(timeout=self.timeout) as client:
                response = await client.post(
                    url,
                    headers={
                        "Authorization": f"Bearer {self.api_key}",
                        "Content-Type": "application/json",
                    },
                    json=payload,
                )
        except httpx.HTTPError as exc:
            raise LLMError(f"LLM request failed: {exc}") from exc
        if response.status_code >= 400:
            raise LLMError(f"LLM HTTP {response.status_code}: {response.text[:1000]}")
        data = response.json()
        choices = data.get("choices") or []
        if not choices:
            raise LLMError(f"LLM response has no choices: {json.dumps(data)[:1000]}")
        message = choices[0].get("message") or {}
        return {"message": message, "usage": data.get("usage", {})}

12.2 coding_tools.py:四个真实工具与安全边界

这是 Agent 的"手"。四个工具全部有真实副作用,因此每一个都有明确边界:路径越界直接拒绝,Patch 锚点必须唯一命中,命令输出强制 UTF-8 解码并用 errors="replace" 兜底 Windows GBK 输出。

python 复制代码
from __future__ import annotations

import subprocess
from dataclasses import dataclass
from pathlib import Path
from typing import Any

from .policy import AgentIdentity, Permission
from .tools import ToolDenied


@dataclass
class CodingTools:
    root: Path

    def __post_init__(self) -> None:
        self.root = self.root.resolve()

    def _safe_path(self, raw: str) -> Path:
        candidate = (self.root / raw).resolve()
        if candidate != self.root and self.root not in candidate.parents:
            raise ToolDenied("path escapes the configured working directory")
        return candidate

    def read_file(self, arguments: dict[str, Any]) -> dict[str, Any]:
        path = self._safe_path(str(arguments.get("path", "")))
        if not path.is_file():
            raise FileNotFoundError(str(path))
        max_chars = min(int(arguments.get("max_chars", 20000)), 100000)
        return {"path": str(path.relative_to(self.root)), "content": path.read_text(encoding="utf-8")[:max_chars]}

    def search_files(self, arguments: dict[str, Any]) -> dict[str, Any]:
        pattern = str(arguments.get("pattern", ""))
        if not pattern:
            raise ValueError("pattern is required")
        results: list[str] = []
        for path in self.root.rglob("*"):
            if path.is_file() and any(part in {".git", ".venv", "node_modules", "__pycache__", ".pytest_cache"} for part in path.parts):
                continue
            if pattern in {"*", "**/*"} or pattern.lower() in path.name.lower():
                results.append(str(path.relative_to(self.root)))
            if len(results) >= 100:
                break
        return {"pattern": pattern, "matches": results}

    def patch_file(self, arguments: dict[str, Any]) -> dict[str, Any]:
        path = self._safe_path(str(arguments.get("path", "")))
        old = str(arguments.get("old", ""))
        new = str(arguments.get("new", ""))
        if not old:
            raise ValueError("old is required")
        content = path.read_text(encoding="utf-8")
        count = content.count(old)
        if count != 1:
            raise ValueError(f"patch anchor must occur exactly once, found={count}")
        path.write_text(content.replace(old, new), encoding="utf-8")
        return {"path": str(path.relative_to(self.root)), "replacements": 1}

    def run_command(self, arguments: dict[str, Any]) -> dict[str, Any]:
        command = str(arguments.get("command", "")).strip()
        if not command:
            raise ValueError("command is required")
        completed = subprocess.run(
            command,
            cwd=self.root,
            shell=True,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
            encoding="utf-8",
            errors="replace",
            timeout=min(int(arguments.get("timeout", 120)), 300),
        )
        return {
            "command": command,
            "exit_code": completed.returncode,
            "stdout": (completed.stdout or "")[-12000:],
            "stderr": (completed.stderr or "")[-12000:],
        }

    def execute(self, identity: AgentIdentity, name: str, arguments: dict[str, Any]) -> dict[str, Any]:
        if Permission.TOOL_EXECUTE not in identity.permissions:
            raise ToolDenied("missing permission: tool.execute")
        handler = {
            "read_file": self.read_file,
            "search_files": self.search_files,
            "patch_file": self.patch_file,
            "run_command": self.run_command,
        }.get(name)
        if handler is None:
            raise ToolDenied(f"unknown coding tool: {name}")
        return handler(arguments)

12.3 cli.py:Agent Loop 与人工审批

这是整个项目的心脏。handle 负责输入护栏与 Memory Recall,_agent_loop 负责多轮 Tool Calling;patch_filerun_command 在执行前必须经过 Approve ... [y/N] 人工确认,最终回答经过输出护栏后写回 Conversation Memory。

python 复制代码
from __future__ import annotations

import argparse
import asyncio
import json
from pathlib import Path
from typing import Any

from .coding_tools import CodingTools
from .llm import LLMError, OpenAICompatibleLLM
from .memory import InMemoryStore, MemoryRecord, MemoryRuntime, RecallQuery
from .policy import AgentIdentity, MemoryAddress, MemoryPolicy, MemoryPolicyEngine, MemoryScope, Permission
from .quality import Guardrails


SYSTEM = """You are a careful terminal coding agent.
You work only inside the supplied cwd. Inspect before editing. Use patch_file with a unique old anchor, never rewrite an entire file. Run tests after changes. Ask for confirmation before patch_file or run_command. Report what you changed and the exact test result."""


class CLI:
    def __init__(self, cwd: Path, llm: OpenAICompatibleLLM) -> None:
        self.cwd = cwd.resolve()
        self.llm = llm
        self.tools = CodingTools(self.cwd)
        self.guardrails = Guardrails()
        self.store = InMemoryStore()
        self.memory = MemoryRuntime(self.store, MemoryPolicyEngine())
        self.identity = AgentIdentity(
            tenant_id="local-tenant",
            subject_id="local-user",
            agent_id="coding-agent",
            permissions=frozenset({Permission.MEMORY_READ, Permission.MEMORY_WRITE, Permission.TOOL_EXECUTE}),
        )
        self.policy = MemoryPolicy(
            readable_scopes=frozenset({MemoryScope.USER}),
            writable_scopes=frozenset({MemoryScope.USER}),
        )
        self.messages: list[dict[str, Any]] = [{"role": "system", "content": SYSTEM}]

    async def handle(self, request: str) -> None:
        incoming = self.guardrails.sanitize_input(request)
        if not incoming.is_safe:
            print("BLOCKED: " + "; ".join(incoming.violations))
            return
        address = MemoryAddress(tenant_id="local-tenant", scope=MemoryScope.USER, subject_id="local-user")
        memories = await self.memory.recall(self.identity, self.policy, RecallQuery(address=address, query=request))
        memory_text = "\n".join(f"- {item.content}" for item in memories)
        self.messages.append({"role": "user", "content": request})
        self.messages.append({"role": "system", "content": f"Relevant memory:\n{memory_text or '(none)'}"})

        try:
            await self._agent_loop(address)
        except Exception as exc:
            print(f"ERROR: {type(exc).__name__}: {exc}")

    async def _agent_loop(self, address: MemoryAddress) -> None:
        for _ in range(12):
            result = await self.llm.complete(self.messages, self.tools.schemas())
            message = result["message"]
            calls = message.get("tool_calls") or []
            if not calls:
                answer = self.guardrails.verify_output(str(message.get("content") or ""))
                print(answer.sanitized_text)
                self.messages.append({"role": "assistant", "content": answer.sanitized_text})
                await self.memory.write_back(self.identity, self.policy, MemoryRecord(address=address, content=answer.sanitized_text[:2000], category="conversation", confidence=0.7, importance=0.3))
                return
            assistant_message = {"role": "assistant", "content": message.get("content") or "", "tool_calls": calls}
            self.messages.append(assistant_message)
            for call in calls:
                fn = call.get("function", {})
                name = fn.get("name", "")
                try:
                    arguments = json.loads(fn.get("arguments") or "{}")
                except json.JSONDecodeError:
                    arguments = {}
                if name in {"patch_file", "run_command"}:
                    answer = input(f"Approve {name}({json.dumps(arguments, ensure_ascii=False)})? [y/N] ").strip().lower()
                    if answer not in {"y", "yes"}:
                        tool_result = {"error": "user denied tool execution"}
                    else:
                        tool_result = self.tools.execute(self.identity, name, arguments)
                else:
                    tool_result = self.tools.execute(self.identity, name, arguments)
                print(f"[{name}] {json.dumps(tool_result, ensure_ascii=False)[:1000]}")
                self.messages.append({"role": "tool", "tool_call_id": call.get("id", name), "name": name, "content": json.dumps(tool_result, ensure_ascii=False)})
        print("Stopped: maximum tool turns reached.")


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--cwd", default=".")
    args = parser.parse_args()
    try:
        llm = OpenAICompatibleLLM.from_env()
        asyncio.run(CLI(Path(args.cwd), llm).run())
    except LLMError as exc:
        raise SystemExit(str(exc)) from exc


if __name__ == "__main__":
    main()

一句话总结这一章的核心观点:

一个能用的 Coding Agent 不需要魔法:模型适配、真实工具、多轮循环加人工审批,四个文件各管一段,剩下的是纪律。


十三、全部真实测试:三个测试与一条真实会话

13.1 Agent Loop 测试(tests/test_cli.py

用 FakeLLM 模拟"模型先发 tool_call、再给最终回答"的两轮行为,验证真实工具真的被执行、工具结果真的回到了消息流:

python 复制代码
import asyncio
from pathlib import Path

from eap.cli import CLI
from eap.llm import OpenAICompatibleLLM


class FakeLLM(OpenAICompatibleLLM):
    def __init__(self):
        super().__init__(api_key="test", base_url="http://test", model="fake")
        self.turn = 0

    async def complete(self, messages, tools):
        self.turn += 1
        if self.turn == 1:
            return {
                "message": {
                    "role": "assistant",
                    "content": "",
                    "tool_calls": [{
                        "id": "call-1",
                        "type": "function",
                        "function": {"name": "read_file", "arguments": '{"path":"README.md"}'},
                    }],
                },
                "usage": {},
            }
        return {"message": {"role": "assistant", "content": "已读取 README.md。"}, "usage": {}}


def test_cli_tool_loop(tmp_path: Path):
    (tmp_path / "README.md").write_text("hello", encoding="utf-8")
    cli = CLI(tmp_path, FakeLLM())
    asyncio.run(cli.handle("读取 README.md"))
    assert cli.llm.turn == 2
    assert any(m.get("role") == "tool" for m in cli.messages)

13.2 工具级端到端测试(tests/test_e2e_tools.py

四个工具逐一真实验证:真实读文件、真实列目录、真实执行中文命令、真实 Patch 临时文件并回读确认,测完自动清理:

python 复制代码
import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "src"))

from eap.coding_tools import CodingTools
from eap.policy import AgentIdentity, Permission


def test_coding_tools_full():
    tools = CodingTools(ROOT)
    identity = AgentIdentity(
        tenant_id="local",
        subject_id="local-user",
        agent_id="test-agent",
        permissions=frozenset({Permission.TOOL_EXECUTE}),
    )

    # 1. search_files:真实遍历项目目录
    s_res = tools.execute(identity, "search_files", {"pattern": "*"})
    assert len(s_res["matches"]) > 0
    print("[PASS] search_files matches count:", len(s_res["matches"]))

    # 2. read_file:真实读取 pyproject.toml
    r_res = tools.execute(identity, "read_file", {"path": "pyproject.toml"})
    assert "enterprise-agent-platform" in r_res["content"]
    print("[PASS] read_file successfully read pyproject.toml")

    # 3. run_command:Windows 中文与 GBK 输出兜底
    c_res = tools.execute(identity, "run_command", {"command": "echo 测试中文字符与命令执行 && dir"})
    assert c_res["exit_code"] == 0
    assert len(c_res["stdout"]) > 0
    print("[PASS] run_command with unicode & gbk handling: exit_code 0")

    # 4. patch_file:唯一锚点替换 + 回读验证 + 自动清理
    test_file = ROOT / "tests" / "temp_patch_test.txt"
    test_content = chr(10).join(["line 1", "old_target_text", "line 3"])
    test_file.write_text(test_content, encoding="utf-8")
    try:
        p_res = tools.execute(identity, "patch_file", {
            "path": str(test_file.relative_to(ROOT)),
            "old": "old_target_text",
            "new": "new_repaired_text",
        })
        assert p_res["replacements"] == 1
        assert "new_repaired_text" in test_file.read_text(encoding="utf-8")
        print("[PASS] patch_file anchor replacement verified")
    finally:
        if test_file.exists():
            test_file.unlink()

13.3 真实执行结果

平台集成测试(上一章的 Gateway 全链路)与上述两个测试一起跑:

text 复制代码
============================= test session starts =============================
platform win32 -- Python 3.11.15, pytest-9.1.1, pluggy-1.6.0
rootdir: G:\tmp\enterprise-agent-platform
configfile: pyproject.toml
collected 3 items

tests/test_cli.py::test_cli_tool_loop PASSED                             [ 33%]
tests/test_e2e_tools.py::test_coding_tools_full PASSED                   [ 66%]
tests/test_integration.py::test_enterprise_agent_platform_integration PASSED [100%]

============================== 3 passed in 0.16s ==============================

工具级端到端单独运行的真实输出:

text 复制代码
[PASS] search_files matches count: 30
[PASS] read_file successfully read pyproject.toml
[PASS] run_command with unicode & gbk handling: exit_code 0
[PASS] patch_file anchor replacement verified
[SUCCESS] ALL CODING TOOLS VERIFICATION 100% PASS!

13.4 真实模型会话实录

以下是接入真实模型(GLM-5.2-CL,OpenAI 兼容 Tool Calling)后一段未经删改的完整终端日志,第一人称输入 测试,Agent 自主决定列目录、读配置、读 README、列出源码文件,最后主动运行 pytest(长输出行按原样截断于终端窗口):

text 复制代码
> 测试
Approve run_command({"command": "ls -la"})? [y/N] y
[run_command] {"command": "ls -la", "exit_code": 0, "stdout": "total 29\ndrwxr-xr-x 1 Alben 197121    0  9月  9 02:41 .\ndrwxr-xr-x 1 Alben 197121    0  9月 12 10:00 ..\ndrwxr-xr-x 1 Alben 197121    0  9月  8 02:49 .pytest_cache\n-rw-r--r-- 1 Alben 197121  190  9月  9 02:41 pyproject.toml\n-rw-r--r-- 1 Alben 197121 1786  9月  9 02:41 README.md\ndrwxr-xr-x 1 Alben 197121    0  9月  9 02:43 src\ndrwxr-xr-x 1 Alben 197121    0  9月 12 10:03 tests\n", "stderr": ""}
[read_file] {"path": "pyproject.toml", "content": "[project]\nname = \"enterprise-agent-platform\"\nversion = \"0.2.0\"\nrequires-python = \">=3.11\"\ndependencies = [\"pydantic>=2,<3\", \"httpx>=0.27,<1\"]\n\n[tool.pytest.ini_options]\npythonpath = [\"src\"]\n"}
[read_file] {"path": "README.md", "content": "# Enterprise Agent Platform\n\nA local Claude Code-style CLI reference implementation. It connects:\n\n```text\nLLM tool-calling loop\n  -> Guardrails\n  -> Tenant / Memory Policy\n  -> real file/search/patch/test tools\n  -> Workflow state\n  -> Trace / Evaluation\n  -> Memory write-back\n```\n\n## Configure\n\nSet these environment variables in Git Bash (do not commit the key):\n\n```bash\nexport EAP_API_KEY='your-api-key'\nexport EAP_BASE_URL='https://www.xxxx.xx/v1'\nexport EAP_MODEL='GLM-5.2-CL'\n```\n\nPowerShell:\n\n```powershell\n$env:EAP_API_KEY = 'your-api-key'\n$env:EAP_BASE_URL = 'https://www.xxxxx.xx/v1'\n$env:EAP_MODEL = 'GLM-5.2-CL'\n```\n\nThe project uses the OpenAI-compatible `POST /chat/completions` endpoint. If the provider exposes a different endpoint or tool-calling schema, adapt only `src/eap/llm.py`.\n\n## Start the CLI\n\n```bash\ncd G:/tmp/enterprise-agent-platform\npython -m pip install -e .\nPYTHONPATH=sr
Approve run_command({"command": "find src tests -type f"})? [y/N] y
[run_command] {"command": "find src tests -type f", "exit_code": 0, "stdout": "src/eap/app.py\nsrc/eap/cli.py\nsrc/eap/coding_tools.py\nsrc/eap/gateway.py\nsrc/eap/llm.py\nsrc/eap/memory.py\nsrc/eap/policy.py\nsrc/eap/quality.py\nsrc/eap/tools.py\nsrc/eap/workflow.py\nsrc/eap/__init__.py\nsrc/eap/__pycache__/app.cpython-311.pyc\nsrc/eap/__pycache__/cli.cpython-311.pyc\nsrc/eap/__pycache__/cli.cpython-312.pyc\nsrc/eap/__pycache__/coding_tools.cpython-311.pyc\nsrc/eap/__pycache__/coding_tools.cpython-312.pyc\nsrc/eap/__pycache__/gateway.cpython-311.pyc\nsrc/eap/__pycache__/llm.cpython-311.pyc\nsrc/eap/__pycache__/llm.cpython-312.pyc\nsrc/eap/__pycache__/memory.cpython-311.pyc\nsrc/eap/__pycache__/memory.cpython-312.pyc\nsrc/eap/__pycache__/policy.cpython-311.pyc\nsrc/eap/__pycache__/policy.cpython-312.pyc\nsrc/eap/__pycache__/quality.cpython-311.pyc\nsrc/eap/__pycache__/quality.cpython-312.pyc\nsrc/eap/__pycache__/tools.cpython-311.pyc\nsrc/eap/__pycache__/tools.cpython-312.pyc\nsrc/eap/__pycache__/wo
Approve run_command({"command": "cd /g/tmp/enterprise-agent-platform && python -m pytest -q 2>&1 | tail -40"})? [y/N] y
[run_command] {"command": "cd /g/tmp/enterprise-agent-platform && python -m pytest -q 2>&1 | tail -40", "exit_code": 1, "stdout": "", "stderr": "ϵͳ\udcd5\udcd2\udcb2\udcbb\udcb5\udcbd\udcd6\udcb8\udcb6\udca8\udcb5\udcc4\udcc2\udcb7\udcbe\udcb6\udca1\udca3\n"}
Approve run_command({"command": "python -m pytest -q 2>&1 | tail -40"})? [y/N] y
[run_command] {"command": "python -m pytest -q 2>&1 | tail -40", "exit_code": 0, "stdout": "...                                                                      [100%]\n3 passed in 0.06s\n", "stderr": ""}
一切正常!测试结果如下:

## 项目概览

**Enterprise Agent Platform** (v0.2.0) --- 一个本地 Claude Code 风格的 CLI 参考实现,包含:

| 模块 | 说明 |
|------|------|
| `cli.py` | 命令行交互入口 |
| `app.py` / `gateway.py` | HTTP 网关 |
| `llm.py` | LLM 工具调用循环 |
| `coding_tools.py` / `tools.py` | 文件/搜索/补丁/测试工具 |
| `policy.py` | 租户/内存策略 + Guardrails |
| `memory.py` | 记忆回写 |
| `workflow.py` | 工作流状态 |
| `quality.py` | Trace / 评估 |

## 测试结果

3 passed in 0.06s

复制代码
✅ **全部 3 个测试通过**,项目状态良好。测试文件包括:
- `tests/test_cli.py`
- `tests/test_e2e_tools.py`
- `tests/test_integration.py`

如果你需要我做什么具体的操作(比如查看某个模块代码、修复问题、添加功能等),请告诉我!

这段日志里值得注意的四个工程事实:

  • 🔸 模型自主决策:没有写任何一行"先跑 pytest"的代码,是模型读完项目结构后自己决定运行测试;
  • 🔸 人工审批真实生效 :四次 run_command 全部经过 Approve ... [y/N] 确认,模型无法绕过;
  • 🔸 工具结果真实回传 :模型总结的测试数字来自 run_command 返回的真实 stdout,不是幻觉;
  • 🔸 失败也是真实链路的一部分 :模型第一次用 Git Bash 风格的 /g/tmp/... 路径调 pytest,cmd 报"系统找不到指定的路径",它没有重试同一条路,而是换成相对路径一次通过------自我纠错发生在真实执行反馈之后。

总结

代码可复制、测试可复跑、日志原样可查------这才叫真正可运行的技术文章。读者的信任,来自每一条命令都能执行、每一份代码都能复现,而不是几张精心挑选的截图。

至此,《企业级 Agent》系列也即将告一段落,只剩下最后一篇。

最后一篇不会再去堆砌各种新概念,也不会刻意展示复杂的技术栈,而是希望站在工程实践的角度,和大家聊一聊 Agent 开发这件事本身。

过去一年,Agent 被包装得越来越神秘,仿佛只有最前沿的团队才能参与。但当真正深入到企业落地后,你会发现,它并没有想象中那么高大上。

Agent 的本质,依然是软件工程。

它需要的是清晰的架构设计、稳定的执行流程、合理的上下文管理、可靠的 Memory,以及一套能够长期维护和持续演进的工程体系,而不是几段 Prompt 或几个热门框架。

希望通过最后一篇文章,能够帮助大家跳出营销宣传和概念包装,以更加理性的视角理解 Agent,真正知道它能解决什么问题、不能解决什么问题,以及企业应该如何一步一步把 Agent 落地到真实业务之中。

技术终将回归工程,工程终将回归价值。 这也是整个《企业级 Agent》系列最想传达的内容。

下一篇:《终章实战|300 行纯 Python 代码,手搓一个 Coding Agent》

相关推荐
VIP_CQCRE1 小时前
Visual Studio 也能接入 AI 编程:用 Ace Data Cloud 快速配置 LMLocal
openai·ai编程·visual studio·ace data cloud
隔窗听雨眠2 小时前
Dify基于TiDB的数据架构重构实践:从数十万容器到统一存储的系统性演进
重构·架构·tidb
fb_123452 小时前
Docker入门-第1-2章-容器生态系统与架构详解
docker·容器·架构
维核科技3 小时前
本地优先的 AI:不联网也能跑的模型和智能体
人工智能
七夜zippoe3 小时前
Function Calling 深度解析:从参数定义到错误处理的完整实践
人工智能·ai·agent·function·calling
zhou lily9 小时前
高可用(HA)架构:在混合云环境下,如何通过多活容灾保障核心业务流的稳定性
架构
m0_734571769 小时前
深入理解人工智能 chatGPT的软件架构
人工智能
飞塔老梅子9 小时前
09. Codex 节省Token ❀ 老梅子学AI
人工智能·ai·token·模型·codex
HyperAI超神经9 小时前
扰动实验+迁移学习,MIT团队推出IRIS,用「指纹」重建单细胞信号传导历史
人工智能·深度学习·机器学习·迁移学习