走向 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.py、memory.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 | recall、append、compare_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 不允许整文件覆盖,只接受 old 和 new 两个片段,并且要求 old 在文件中只出现一次。run_command 和 patch_file 都会在 CLI 中询问:
text
Approve patch_file({...})? [y/N]
Approve run_command({...})? [y/N]
这还不是 gVisor 或 Firecracker 级别的生产沙箱,但已经比直接把 shell=True 暴露给模型多了一层工作目录边界、用户审批和输出截断。
真实使用方式
进入 CLI 后,可以这样使用:
text
> 读取 README.md,解释这个项目的结构,不要修改文件。
> 找出失败的测试,定位根因,只做最小 Patch,然后运行测试。
> 检查最近修改的 Python 文件,先给我风险分析,不要执行命令。
当模型返回 read_file 或 search_files 时,CLI 会自动执行并把结果回传给模型;当模型尝试 patch_file 或 run_command 时,CLI 会暂停等待人工批准。这个交互顺序才是 Claude Code 风格 Coding Agent 的基本形态。
十二、全部真实代码:CLI 三大核心模块
这一章给出 CLI 的全部真实源码。这些代码不是文章示意,而是上一节那条实测会话正在执行的代码,可以直接复制运行。
12.1 llm.py:OpenAI 兼容模型适配层
整个模型层只有一个类:从环境变量读配置,向 /chat/completions 发送带 tools 的请求,返回 message 与 usage。没有任何 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_file 和 run_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》。