用 Java 写 Agent 的团队,大多卡在同一个坎上:Demo 跑得挺欢,一上生产就出事。模型会自己决定调不调工具、调哪个工具,重启一次会话全丢,上下文越滚越长最后撑爆 token。阿里开源的 AgentScope Java 把这些问题当成框架级问题来解决------工具注册、三态权限、会话持久化、上下文压缩,全都有现成件。这篇文章用一个订单助手的例子,把这些能力一个个装上,代码可以直接跑。
文 / 会编程的吕洞宾
一、这个问题到底是什么
先说清楚裸的 Agent 循环为什么上不了生产。
一个最朴素的 Agent,就是「让模型自己决定调哪个方法」:你给它一堆工具描述,它选一个,你执行,把结果喂回去,它再选。LangChain4j、Spring AI 都能做这件事,几十行代码就能跑通。
问题出在四个地方。
**第一,模型会乱调工具。**你给了它 read_logs、restart_service、delete_order 三个工具,它会老老实实先读日志再重启吗?不一定。它完全可能直接调 delete_order,因为它觉得这样最省事。工具描述里写了「危险操作」也没用,那只是自然语言提示,不是强制约束。
**第二,会话状态说没就没。**你把 Agent 对象当单例放在 Spring 容器里,第一次请求完了,第二次请求进来,它什么都不记得。想让它记得,就得自己做会话存储、自己拼历史消息、自己注意多用户隔离------一旦有人把用户 A 的对话喂给了用户 B,那就是事故。
**第三,上下文会越滚越长。**长任务跑几十轮,工具返回的结果全是几 KB 的 JSON,全塞进 prompt,最后模型还没开始思考,token 就已经烧掉一大半。
**第四,出事没法查。**模型到底什么时候调了工具、调的参数是什么、被拒绝了还是执行了,全靠打日志猜。前端想做一个「AI 正在调用工具...」的实时效果,更不知道从哪儿接。
AgentScope Java 的思路是:这些问题不该让每个业务团队自己写一遍。它把 Agent 拆成两层------一个只管「推理-行动」的 ReActAgent,和一个在它外面套了一堆工程能力的 HarnessAgent。权限、会话、压缩、子代理、沙箱,都是可插拔的中间件,挂在循环外面,不碰核心算法。
一句话概括:ReActAgent 解决「能不能干活」,HarnessAgent 解决「能不能一直安全地干活」。
二、底层原理到底怎么回事
工具的三层结构
AgentScope 里工具不是一个方法,是三个概念。
- Tool :具体能力。你可以写一个普通类,方法上打
@Tool注解,框架用反射把参数类型转成 JSON Schema 交给模型;复杂场景就继承ToolBase,手写 schema 和权限逻辑。 - Toolkit:装工具的容器。它负责把注册进来的工具打包成模型能看懂的函数列表,收到调用请求后再分发到对应的工具对象。
- Tool Group:一组工具的名字集合,可以整体启用或停用。这个设计很实用------工具多了以后,几十个 schema 全塞给模型,它选错的概率会上升,而且白烧 token。
用 Toolkit#registerTool(Object) 注册的普通工具,会自动进入一个叫 basic 的保留分组,永远可用。需要按场景切换的工具,单独建成 group。
权限系统的三级判断
权限这块是 AgentScope 最有价值的部分,它不是「加个 if」那么简单,而是一个完整的决策引擎,每次工具调用都要过一遍,最后只可能是三个结果:ALLOW(放行)、ASK(停下来问人)、DENY(拒绝)。
决策依据有三样东西,优先级从高到低:
- 规则(Rules) :显式配的策略,比如「
restart_service传参匹配prod-*就 ASK」「payment-*直接 DENY」。规则优先级最高,命中就定了。 - 模式(Mode):全局兜底策略。匹配不到任何规则的时候,看模式怎么定。
- 工具自带的运行时检查 :写在
ToolBase#checkPermissions里的逻辑,比如「路径在危险目录里就 ASK」。这一层是硬编码在工具里的,规则和模式都绕不过去,这是为了防止有人用宽松模式把安全检查整个关掉。
模式有五种,实际项目里最常用的是这几种:
| 模式 | 行为 | 什么场景用 |
|---|---|---|
DEFAULT |
没规则就 ASK | 有真人盯着的时候,最稳 |
EXPLORE |
只读:读放行,写和命令全拒 | 让 Agent 先规划、先看代码 |
BYPASS |
基本全放行(DENY 规则仍然生效) | 完全可信的沙箱环境 |
DONT_ASK |
把 ASK 降级成 DENY | 定时任务、无人值守 |
DONT_ASK 这个模式值得单独说。因为无人值守时没人能点「同意」,如果还维持 ASK,Agent 就会一直挂在那里等。降级成 DENY 之后,没被显式允许的动作直接拒绝,Agent 会收到拒绝结果然后想办法绕路或者告诉你干不了------这是可控的失败。
ASK 触发的时候,Agent 不只是问一句「行不行」,它会顺带生成「建议规则」。用户点了同意并接受建议规则之后,同样的调用下次就自动放行了。这相当于 Agent 自己长出了权限配置,省得你手工一条条写。
状态管理:三个对象各管一摊
很多人搞不清 RuntimeContext、AgentState、AgentStateStore 的区别,其实它们对应三种生命周期的数据:
RuntimeContext:这一次调用是谁在说话。里面是sessionId、userId,还能塞自定义对象(比如登录用户信息)。它不持久化,一次调用结束就没了。AgentState:这次会话的运行时快照,包含对话上下文、权限规则、当前任务状态。每次call()结束自动存,下次自动加载。AgentStateStore:状态存在哪儿。默认是本地 JSON 文件(~/.agentscope/state/<agentId>/),单机够用;集群上要换成 Redis 或者数据库实现,让不同副本共享同一份会话。
关键设计是:Agent 对象本身是无状态的 。同一个 Agent 实例可以同时服务不同用户,靠 RuntimeContext 里的 (userId, sessionId) 定位状态,互不干扰。同一个会话的并发请求会被自动串行化,不同会话并行跑。
这意味着你可以把 Agent 做成单例 Spring Bean,水平扩容几台机器都行------只要状态存储是共享的。
事件流:把内部动作拍给你看
Agent 干活的每一步都会发事件,一共三十来种,涵盖模型调用、文本增量、工具开始执行、工具结果、需要用户确认等等。你订阅 streamEvents() 就能拿到这些事件:前端做打字机效果用 TEXT_BLOCK_DELTA,做「正在调用 XX 工具」的提示用 TOOL_CALL_START,需要人工审批的场景监听确认事件。
这比「拿一个最终字符串」强太多------用户能看见 Agent 在想什么、在干什么,等待焦虑直接减半。
三、实战:手把手写代码
示例一:最小可用的 Agent,带一个自定义工具
先上依赖。用 BOM 统一管理版本,三个坐标写全:
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>agentscope-demo</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<agentscope.version>2.0.3</agentscope.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.agentscope</groupId>
<artifactId>agentscope-bom</artifactId>
<version>2.0.3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Agent 框架 + 工程能力(workspace / 记忆 / 会话持久化 / 子代理) -->
<dependency>
<groupId>io.agentscope</groupId>
<artifactId>agentscope-harness</artifactId>
<version>2.0.3</version>
</dependency>
<!-- 模型接入,这里用阿里云百炼;换 OpenAI 就换这个 artifact -->
<dependency>
<groupId>io.agentscope</groupId>
<artifactId>agentscope-extensions-model-dashscope</artifactId>
<version>2.0.3</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.16.0</version>
<configuration>
<release>17</release>
</configuration>
</plugin>
</plugins>
</build>
</project>
JDK 至少 17。下面这个类完整可跑,做的事是:注册一个查订单的工具,问两轮问题,第二轮验证会话记忆。
java
package com.example.agentscope;
import io.agentscope.core.agent.RuntimeContext;
import io.agentscope.core.message.UserMessage;
import io.agentscope.core.tool.Tool;
import io.agentscope.core.tool.ToolParam;
import io.agentscope.core.tool.Toolkit;
import io.agentscope.harness.agent.HarnessAgent;
import java.nio.file.Paths;
import java.util.Map;
public class FirstAgentDemo {
/**
* 普通 Java 类,方法打上 @Tool 就能被模型调用。
* readOnly / concurrencySafe 是给框架看的元信息,不是给模型看的。
*/
public static class OrderTools {
private static final Map<String, String> ORDERS = Map.of(
"A1001", "订单 A1001:MacBook Pro,金额 19999 元,状态已发货",
"A1002", "订单 A1002:机械键盘,金额 699 元,状态待发货");
@Tool(
name = "query_order",
description = "根据订单号查询订单的详情和状态",
readOnly = true,
concurrencySafe = true)
public String queryOrder(
@ToolParam(name = "orderId", description = "订单号,例如 A1001")
String orderId) {
String order = ORDERS.get(orderId);
return order == null ? "没有查到订单:" + orderId : order;
}
}
public static void main(String[] args) {
// 1. 把工具注册进 Toolkit
Toolkit toolkit = new Toolkit();
toolkit.registerTool(new OrderTools());
// 2. 构建 Agent。model 传字符串,框架自动读 DASHSCOPE_API_KEY
HarnessAgent agent = HarnessAgent.builder()
.name("order-assistant")
.sysPrompt("你是订单客服助手,回答订单问题前必须先调用 query_order 查询真实数据。")
.model("dashscope:qwen-plus")
.toolkit(toolkit)
.workspace(Paths.get(".agentscope/workspace"))
.build();
// 3. 同一次会话的两次提问,用同一个 RuntimeContext 定位
RuntimeContext ctx = RuntimeContext.builder()
.sessionId("session-001")
.userId("alice")
.build();
agent.call(new UserMessage("帮我查一下订单 A1001 现在什么状态?"), ctx).block();
// 第二轮不说订单号,看它能不能从上一轮记住
agent.call(new UserMessage("刚才那个订单多少钱?"), ctx).block();
// 进程重启后再用同一个 sessionId 提问,历史仍然在
}
}
运行前把 API Key 放进环境变量:
bash
export DASHSCOPE_API_KEY=sk-xxxxxx
mvn -q compile exec:java -Dexec.mainClass=com.example.agentscope.FirstAgentDemo
几个关键点:
.model("dashscope:qwen-plus")里的字符串由框架的ModelRegistry解析,它会自动找对应的环境变量(OpenAI 找OPENAI_API_KEY,Anthropic 找ANTHROPIC_API_KEY)。要换模型只改这一个字符串。.toolkit(toolkit)才是把工具交给 Agent 的动作,光注册不挂上去,模型看不到。- 会话状态默认落在
~/.agentscope/state/<agentName>/<userId>/<sessionId>/agent_state.json,在 workspace 目录之外。这个设计是有意的:状态是恢复 workspace 的前提,不能和 workspace 数据缠在一起。 sysPrompt里那句「必须先调用 query_order 查询真实数据」很有必要。模型有时候会凭常识编答案,尤其是问它「订单多少钱」这种它没有的信息。
提示:工具返回的字符串不要太长。如果你要返回一个大 JSON,记得在工具里先裁剪字段,否则上下文很快就被撑满。
示例二:给危险操作加权限管控
这个例子模拟运维助手:可以读日志,可以重启服务,但重启支付服务的请求必须直接拒绝。
java
package com.example.agentscope;
import io.agentscope.core.agent.RuntimeContext;
import io.agentscope.core.message.UserMessage;
import io.agentscope.core.permission.PermissionBehavior;
import io.agentscope.core.permission.PermissionContextState;
import io.agentscope.core.permission.PermissionMode;
import io.agentscope.core.permission.PermissionRule;
import io.agentscope.core.tool.Tool;
import io.agentscope.core.tool.ToolParam;
import io.agentscope.core.tool.Toolkit;
import io.agentscope.harness.agent.HarnessAgent;
import java.nio.file.Paths;
public class PermissionDemo {
public static class OpsTools {
@Tool(
name = "read_logs",
description = "读取指定服务的最近日志",
readOnly = true,
concurrencySafe = true)
public String readLogs(
@ToolParam(name = "service", description = "服务名,例如 order-service")
String service) {
return "【" + service + " 最近 100 行日志】...模拟日志内容...";
}
@Tool(
name = "restart_service",
description = "重启指定服务,会中断该服务的现有请求",
readOnly = false,
concurrencySafe = false)
public String restartService(
@ToolParam(name = "service", description = "服务名,例如 order-service")
String service) {
return "服务 " + service + " 已重启完成";
}
}
public static void main(String[] args) {
Toolkit toolkit = new Toolkit();
toolkit.registerTool(new OpsTools());
// 权限规则:读日志永远放行;重启 prod-* 要人确认;重启 payment-* 直接拒绝
PermissionContextState permissionContext = PermissionContextState.builder()
.mode(PermissionMode.DEFAULT)
.addAllowRule("read_logs",
new PermissionRule("read_logs", null, PermissionBehavior.ALLOW, "policy"))
.addAskRule("restart_service",
new PermissionRule("restart_service", "prod-*", PermissionBehavior.ASK, "policy"))
.addDenyRule("restart_service",
new PermissionRule("restart_service", "payment-*", PermissionBehavior.DENY, "policy"))
.build();
HarnessAgent agent = HarnessAgent.builder()
.name("ops-assistant")
.sysPrompt("你是运维助手,可以查日志和重启服务。")
.model("dashscope:qwen-plus")
.toolkit(toolkit)
.permissionContext(permissionContext)
.workspace(Paths.get(".agentscope/workspace"))
.build();
RuntimeContext ctx = RuntimeContext.builder()
.sessionId("ops-001")
.userId("bob")
.build();
// 读日志:命中 ALLOW,直接执行
agent.call(new UserMessage("帮我看看 order-service 最近的日志"), ctx).block();
// 重启支付服务:命中 DENY,模型会收到拒绝结果并解释给你听
agent.call(new UserMessage("支付服务卡住了,重启一下 payment-service"), ctx).block();
}
}
PermissionRule 的四个字段分别是:工具名、匹配内容(null 表示这个工具的所有调用都算命中)、行为、规则来源。匹配内容交给工具自己的 matchRule() 去判断,所以它能理解通配符这种模式。
规则不冲突的优先级是 DENY > ASK > ALLOW。也就是说,你写了一条「restart_service 全放行」,再写一条「payment-* 拒绝」,支付服务还是会被拦住------拒绝永远赢。这个设计很关键,线下配了一堆放行规则之后,加一条兜底拒绝就能封住漏洞。
再补一个无人值守的变体。定时任务凌晨跑,没人点同意,就把模式换成下面这样:
java
PermissionContextState nightShift = PermissionContextState.builder()
.mode(PermissionMode.DONT_ASK) // ASK 降级为 DENY,绝不挂起等待
.addAllowRule("read_logs",
new PermissionRule("read_logs", null, PermissionBehavior.ALLOW, "policy"))
.addAllowRule("restart_service",
new PermissionRule("restart_service", "staging-*", PermissionBehavior.ALLOW, "policy"))
.build();
只有预发环境的重启被放行,别的动作一律拒绝。Agent 收到拒绝会退而求其次,比如只报告问题不动作。
示例三:生产模板------共享会话 + 上下文压缩 + 子代理
最后一个例子面向多副本部署。三个变化:状态存到 Redis、历史超长自动压缩、把重活丢给子代理。
java
package com.example.agentscope;
import io.agentscope.core.agent.RuntimeContext;
import io.agentscope.core.message.UserMessage;
import io.agentscope.core.memory.compaction.CompactionConfig;
import io.agentscope.core.memory.compaction.ToolResultEvictionConfig;
import io.agentscope.core.permission.PermissionContextState;
import io.agentscope.core.permission.PermissionMode;
import io.agentscope.core.tool.Toolkit;
import io.agentscope.extensions.redis.RedisDistributedStore;
import io.agentscope.harness.agent.DistributedStore;
import io.agentscope.harness.agent.HarnessAgent;
import io.agentscope.harness.agent.sandbox.impl.docker.DockerFilesystemSpec;
import io.agentscope.harness.agent.IsolationScope;
import java.nio.file.Paths;
import redis.clients.jedis.JedisPooled;
public class ProductionAgentDemo {
public static void main(String[] args) {
// 1. 分布式存储:一行把会话状态、沙箱快照、执行锁全接上 Redis
JedisPooled jedis = new JedisPooled(System.getenv("REDIS_URI"));
DistributedStore store = RedisDistributedStore.fromJedis(jedis);
Toolkit toolkit = new Toolkit();
toolkit.registerTool(new PermissionDemo.OpsTools());
// 2. Agent 做成单例,进程内无状态,多副本共享同一个 Redis
HarnessAgent agent = HarnessAgent.builder()
.name("ops-assistant")
.sysPrompt("你是运维助手,长任务先规划再执行,能委派的活交给子代理。")
.model("dashscope:qwen-plus")
.toolkit(toolkit)
.workspace(Paths.get("/var/agentscope/workspace"))
.distributedStore(store)
// 沙箱隔离:每个用户一个独立环境,容器里跑命令
.filesystem(new DockerFilesystemSpec()
.image("eclipse-temurin:21-jre")
.isolationScope(IsolationScope.USER))
// 历史超过 50 条就开始压缩,保留最近 20 条
.compaction(CompactionConfig.builder()
.triggerMessages(50)
.keepMessages(20)
.build())
// 工具返回的巨大结果写磁盘,上下文里只留摘要
.toolResultEviction(ToolResultEvictionConfig.defaults())
.permissionContext(PermissionContextState.builder()
.mode(PermissionMode.DEFAULT)
.build())
.build();
// 3. 请求进来,从 RuntimeContext 取身份,其余交给框架
String userId = "alice";
String sessionId = "session-20260919-001";
agent.call(new UserMessage("巡检一下订单链路,有问题先给我结论"),
RuntimeContext.builder().sessionId(sessionId).userId(userId).build())
.block();
// 换台机器、换个副本,只要 Redis 是同一个,同一会话就能接着聊
agent.streamEvents(new UserMessage("接着说,你打算怎么修"),
RuntimeContext.builder().sessionId(sessionId).userId(userId).build())
.doOnNext(event -> System.out.println("[event] " + event.getType()))
.blockLast();
}
}
要跑这个例子,pom 里再加上 Redis 扩展和 Jedis(版本和 BOM 保持一致):
xml
<dependency>
<groupId>io.agentscope</groupId>
<artifactId>agentscope-extensions-redis</artifactId>
<version>2.0.3</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>8.0.1</version>
</dependency>
子代理不用写 Java 代码,在 workspace 里丢一个文件就行。文件名就是子代理的 ID:
.agentscope/workspace/subagents/log-analyst.md
markdown
---
description: 日志分析专家。需要排查线上报错、定位异常日志时使用。
steps: 8
tools: [read_logs]
---
你是日志分析专家。拿到排查任务后:
1. 先调 read_logs 拉取相关服务日志
2. 找出异常堆栈的第一现场,不要只看最后一行报错
3. 输出结论:根因 + 受影响范围 + 建议动作
父 Agent 在推理时会看到这个子代理的工具描述,然后自己决定要不要 agent_spawn 把它拉起来干活。子代理在独立会话里跑,干完只把结论还回来,父 Agent 的上下文不会被几万行日志污染。
如果 workspace 目录里同时有 AGENTS.md,它会在每次推理时重新注入系统提示------你改完文件不用重启,下一次调用就生效。
四、踩坑经验和最佳实践
坑一:只注册工具不挂 Toolkit。 new Toolkit() 之后忘了 .toolkit(toolkit),模型完全不知道有工具存在,表现是它一本正经地用嘴回答,说「我建议你查一下订单」------查了个寂寞。先检查这个。
**坑二:把工具当数据库用。**工具返回几 MB 的 JSON,几轮下来上下文就爆。要么在工具里裁剪字段,要么开 toolResultEviction 让它把大结果写盘、上下文里只留摘要。工具返回值的长度,直接决定你的 token 账单。
**坑三:用 EXPLORE 模式跑需要动手的任务。**这个模式是只读的,写操作全拒。有人拿它当「安全模式」用,结果 Agent 一直在拒绝里打转,还以为模型变笨了。EXPLORE 的定位是规划阶段和代码阅读,不是省电模式。
**坑四:sessionId 用固定字符串。**所有用户共用一个 session-001,历史互相串味,问着问着自己都不知道在跟谁说话。sessionId 必须和业务会话绑定,userId 必须和登录用户绑定,两个都不能省。
**坑五:多副本还用本地文件存状态。**默认的 JSON 文件存储只适合开发和单机。两个副本轮询,用户在 A 副本说话、B 副本回答,B 副本压根不知道前面聊了啥,表现是「随机失忆」。上集群必须换 Redis 或数据库实现。
几个我推荐的用法:
- 权限从紧到松,别反过来。 上线先用
DEFAULT,观察一段时间日志,看模型实际调了哪些工具、哪些参数,再针对性加放行规则。一上来就BYPASS,等于把车钥匙交给一个刚拿驾照的实习生。 - 工具的
description当产品文案写。 模型选工具全靠这段文字。「重启服务」和「重启指定服务,会中断该服务现有请求,生产环境需谨慎」相比,后者被误调的几率明显更低。 readOnly = true要老老实实标。 框架会用它做判断(只读的调用更容易被自动放行),标错了等于给写操作开了后门。- 给长任务开 Plan Mode。 让 Agent 先在只读状态下把计划写出来给你确认,确认完再进执行状态,比它一路闷头干完要好得多。
五、性能对比和技术选型
三个框架怎么选,我的看法比较直接:
| 维度 | AgentScope Java | Spring AI | LangChain4j |
|---|---|---|---|
| 定位 | 企业级 Agent 运行时 | Spring 生态的模型抽象 | 轻量级 LLM 编排 |
| 权限管控 | 内置三态引擎 + 规则 | 靠自己写 | 护栏组件(护栏偏输入输出过滤) |
| 会话持久化 | 内置,支持 Redis/MySQL/Postgres | 需自己接 ChatMemory 存储 | 需自己接 ChatMemoryStore |
| 长任务能力 | workspace / 压缩 / 子代理 / 沙箱 | 偏单轮能力组合 | 编排靠手写 |
| 学习成本 | 中等偏高,概念多 | 低,Spring 开发者上手快 | 低 |
| 适合场景 | 长期驻留、多租户、要审计的 Agent | 业务系统里嵌个 AI 问答 | 快速搭原型、流程固定 |
性能上没有本质差别------三者都是调大模型 API 的包装,瓶颈在模型和网络,不在框架。真正拉开差距的是故障恢复和管控能力:AgentScope 的会话状态是可中断、可跨副本恢复的,另外两个你得自己搭这套地基。
选型建议:如果只是给现有 Spring Boot 应用加个问答入口,用 Spring AI,两天能上;如果是做一个要跑好几个月、多个部门共用、出了事要能查的 Agent 平台,AgentScope 更省事------它把最难的那部分(状态、权限、隔离)做成了框架能力。
顺带一句,三者不冲突。模型接入层完全可以复用 Spring AI 的抽象,Agent 编排层用 AgentScope 或者 LangChain4j。
六、总结
把这篇的关键点收一下:
- 裸的 Agent 循环只能跑 Demo。上生产要补四件事:工具调用的权限约束、会话状态持久化、上下文长度控制、可观测的事件流。
- AgentScope 用
ReActAgent+HarnessAgent两层结构解决这些问题,工程能力都挂在循环外面,核心算法不动。 - 权限是三态引擎:规则 > 模式 > 工具内置检查,DENY 永远优先。无人值守场合用
DONT_ASK把 ASK 降级成拒绝,避免任务卡死。 - 会话靠
RuntimeContext里的(userId, sessionId)定位状态,Agent 实例本身无状态,所以能做成单例、能水平扩容。 - 长上下文靠压缩 + 大结果落盘控制;重活交给子代理,父 Agent 的上下文只收结论。
最值得抄走的其实是那个思路转变:**别把 Agent 当成一个会调方法的函数,把它当成一个需要权限、需要记忆、需要审计的常驻服务来设计。**代码写起来差不多,但上线之后是两种命运。