
1. 项目概述为什么一个“小 Agent”值得你花两小时认真拆解最近在技术社区刷到不少人在问“Agent 到底是不是新瓶装旧酒”、“ReAct 框架到底比传统 if-else 强在哪”、“Java 写 Agent 是不是自找麻烦TS 不香吗”——这些问题背后其实藏着一个更本质的困惑我们每天调用的 LLM API、封装的 Prompt 模板、写的工具函数离真正能“思考行动”的智能体究竟还差哪几块砖这个标题里说的“剥开 Agent 开发”不是教你从零造轮子而是以pi-agent一个轻量、开源、结构清晰的 Java/TS 双栈 Agent 实现为解剖样本亲手拆开它的骨架看它怎么把“用户一句话”变成“先查天气、再订会议室、最后发邮件”这一连串有逻辑、有回溯、有容错的动作链。它不追求大模型参数量也不堆砌分布式调度但每一步都踩在 Agent 架构的“关键关节”上——比如决策层如何与工具层解耦、状态如何在多步推理中可靠传递、错误发生时系统怎么不崩而只是重试。我带过三届校招 Java 后端实习生发现一个共性现象90% 的人能熟练写 MyBatis 查询、能配 Spring Boot Starter但一碰到“让程序自己决定下一步该调哪个接口”就卡在“不知道该把判断逻辑写在哪”。这不是能力问题是缺乏对Action-Reasoning Loop即 ReAct 范式的具象认知。而 pi-agent 正好提供了一个“最小可运行闭环”它用不到 500 行核心代码Java 版实现了完整的 Observation → Thought → Action → Observation 四步循环所有工具调用都通过统一接口注入所有中间状态都显式存于 Context 对象中。这意味着——你不需要懂 LangChain 或 LlamaIndex也能看清 Agent 的“心跳节奏”。这篇文章适合三类人Java 工程师想理解如何把已有业务系统比如订单服务、风控引擎快速接入 Agent 流程而不是被 Python 生态绑架前端/TS 开发者正纠结 React Flowork 做可视化 Agent 编排是否靠谱需要知道底层执行器该怎么设计准备面试的应届生或转岗者那些反复出现在字节、阿里、拼多多 Java 面试题里的“如何设计一个可扩展的 Agent 框架”答案就藏在 pi-agent 的 package 结构和接口定义里。接下来我会像带新人 Pair Programming 一样带你一行行过 pi-agent 的核心设计不讲虚概念只说“这段代码为什么这么写”、“如果换种写法会掉进什么坑”。你不需要提前装环境所有结论都基于真实调试日志和线程堆栈截图——毕竟真正的 Agent 开发从来不是复制粘贴而是理解每一行 return 语句背后的权衡。2. 整体架构设计为什么 pi-agent 选择“Java 主干 TS 辅助”而非全栈 TS2.1 核心矛盾LLM 的非确定性 vs 企业系统的强一致性要求很多初学者一上来就想用 Next.js Vercel 搭个“AI 助手网站”结果很快撞墙用户问“查张三上个月报销单”系统返回“已为您生成 PDF”但 PDF 里却是李四的数据。问题不在前端渲染而在后端执行链路失控——当多个异步工具调用查数据库、调第三方 API、写文件混在同一个 LLM 推理上下文中失败重试、超时熔断、状态回滚就变成了黑盒赌博。pi-agent 的破局点很务实把“决策中枢”和“执行引擎”物理隔离。Java 层core模块只做三件事接收原始用户输入 → 调用 LLM 获取结构化 Action 指令 → 解析指令并分发给对应工具 → 汇总工具返回结果生成下一轮 Observation。它不碰任何 UI 渲染、不处理 WebSocket 心跳、不管理 React 组件生命周期。TS 层web模块只做一件事把 Java 提供的 REST API 封装成 React Hook如useAgentExecutor并在 Flowork 画布上可视化拖拽工具节点比如“查询 MySQL”、“发送钉钉消息”。所有前端逻辑都是纯声明式的状态变更完全由 Java 后端的/v1/step接口驱动。提示这种分层不是为了炫技而是解决一个具体痛点——某次压测中我们发现当并发请求超过 200 QPS 时Node.js 进程的 Event Loop 会被长耗时工具调用如 PDF 生成阻塞导致整个 Agent 会话状态错乱。换成 Java 的线程池隔离后单机稳定支撑 800 并发会话且每个会话的 Context 对象生命周期可控基于 ThreadLocal 自定义 Scope。2.2 模块划分五个包名暴露的设计哲学pi-agent 的 Maven 模块结构看似简单实则暗藏玄机包名职责关键设计意图agent.core定义 Agent 运行时核心接口AgentExecutor主流程、Tool工具抽象、Context状态容器强制契约先行所有工具必须实现execute(Context context)方法确保 LLM 输出的 Action 名称能 1:1 映射到 Java 方法杜绝字符串硬编码风险agent.tool内置工具实现DatabaseQueryTool、HttpRequestTool、FileWriteTool工具即插件每个工具类独立打包业务方只需引入tool-mysql依赖即可接入自有数据库无需修改 core 模块agent.llmLLM 适配层OpenAiClient、QwenClient、LocalLlmClient对接 Ollama模型无关性所有 LLM 客户端都实现LlmProvider接口返回标准化的LlmResponse对象含thought、action、actionInput字段屏蔽底层 API 差异agent.memory记忆管理InMemoryConversationStore开发用、RedisConversationStore生产用记忆可替换ConversationStore接口定义save()/load()方法业务方若需审计日志可轻松实现DbConversationStore写入 MySQLagent.webSpring Boot Web 层AgentController、AgentConfigAPI 即协议/v1/execute接收 JSON 输入含userId、sessionId、input返回标准AgentResponse前端无需关心 Java 内部状态流转这种设计直接回答了面试高频题“如何保证 Agent 框架的可扩展性”——答案不是堆设计模式而是用接口切割关注点。比如新增一个“调用飞书审批流”工具你只需要创建FeishuApprovalTool implements Tool在application.yml中配置tool.feishu.appIdxxx启动时Bean FeishuApprovalTool feishuTool()注入 Spring 容器。整个过程不改一行core模块代码连单元测试都不用动——因为Tool接口的execute()方法签名早已约定死输入是Context输出是String即下一步 Observation。2.3 为什么不用纯 TS 实现React Agent 框架图里的“执行器”到底该放哪搜索热词里频繁出现 “react agent框架图”、“react画布 flowork”这说明大量前端开发者正尝试用 React 构建 Agent 可视化编排。但这里有个致命误区把 React 当作执行引擎而非控制台。我见过最典型的反模式案例某团队用 React Zustand 管理 Agent 状态在useEffect里监听currentStep变化自动触发fetch(/api/tool/db-query)。表面看流程清晰实际埋下三颗雷状态漂移用户快速点击“重试”按钮触发多次useEffectZustand 的setState异步更新导致currentStep和真实执行进度错位错误不可追溯某个工具调用超时React 层只能显示“加载中...”无法获取 Java 后端捕获的SQLTimeoutException堆栈安全边界模糊前端直接拼接 SQL 查询条件如WHERE name ${userInput}绕过 Java 层的参数化预编译。pi-agent 的解法很朴素React 只负责“呈现当前步骤”所有决策和执行必须经由 Java 后端。具体到 Flowork 画布它渲染的不是“可执行节点”而是“可配置节点”——当你拖拽一个“数据库查询”节点时画布只生成 JSON 描述{ type: database-query, config: { dataSource: order-db, sql: SELECT * FROM orders WHERE user_id ? } }这份 JSON 被提交到 Java 的/v1/execute接口后由DatabaseQueryTool的execute()方法解析并执行。React 层永远不知道?参数被替换成什么值自然规避了 SQL 注入。注意这不是贬低前端价值而是明确分工。就像汽车仪表盘不参与发动机点火但它必须实时反映水温、转速——Flowork 画布的价值在于让业务人员非程序员能看懂 Agent 正在做什么而不是让它代替 Spring Boot 去连数据库。3. 核心细节解析ReAct 循环的四步如何用 Java 落地3.1 第一步Observation —— 如何把用户输入变成 LLM 能理解的“上下文快照”很多人以为 Observation 就是原样转发用户消息这是最大误解。真正的 Observation 是经过业务语义增强的上下文摘要。pi-agent 的ObservationBuilder类做了三件事注入会话元数据自动添加Current time: 2024-06-15 14:30:22、User role: admin、Last action result: success追加历史摘要不存储完整对话记录避免 token 爆炸而是用 LLM 压缩前 3 轮交互为一句“用户想查北京朝阳区门店库存已确认门店 ID 为 BJ-CY-001”嵌入工具描述动态拼接当前可用工具列表格式严格遵循 ReAct 论文要求Available tools: database-query: Query data from configured databases. Args: {sql: valid SQL with placeholders} http-request: Send HTTP requests to external APIs. Args: {url: full URL, method: GET/POST} file-write: Write content to local file system. Args: {path: /tmp/output.txt, content: text to write}关键细节在于Args 的 JSON Schema 生成。pi-agent 不用手写字符串模板而是用 Jackson 的ObjectMapper反射解析工具类的execute()方法参数public class DatabaseQueryTool implements Tool { public String execute(Context context) { // context.get(sql) 从 Observation 中提取 String sql context.get(sql); return jdbcTemplate.queryForList(sql).toString(); } }ObservationBuilder读取DatabaseQueryTool的方法签名自动生成{sql: valid SQL with placeholders}。这样做的好处是当业务方修改工具参数比如增加timeoutMs字段Observation 描述自动同步无需人工维护文档。实操心得我在某次金融项目中发现客户要求所有 SQL 查询必须带/* trace_idxxx */注释。如果 Observation 是静态字符串就得全局搜索替换而用反射生成方案只需在DatabaseQueryTool.execute()方法参数上加个SqlHint注解ObservationBuilder就能自动注入注释模板。这就是“契约优于配置”的威力。3.2 第二步Thought —— LLM 返回的“思考过程”如何验证其合理性ReAct 的精髓在于 Thought 不是装饰品而是可验证的推理中间态。pi-agent 对 LLM 返回的thought字段做了三层校验第一层结构合法性检查LLM 必须返回符合正则^I need to.*$或^I can use.*$的句子否则视为“胡言乱语”直接抛出InvalidThoughtException。例如✅ 合法I need to check the inventory status of product P123❌ 非法The inventory is low缺少动作意图第二层工具存在性检查解析action字段如database-query是否在ToolRegistry中注册。这里有个精妙设计ToolRegistry不是静态 Map而是 Spring 的ListableBeanFactory支持按Order注解排序。当多个工具同名时比如file-write有本地版和 S3 版优先使用Order(1)的实现。第三层参数完备性检查用 JSON Schema 验证actionInput是否包含必需字段。比如http-request工具要求url和method若 LLM 返回{url: https://api.example.com}缺少method则触发MissingActionInputException并自动构造修复提示Your actionInput is missing required field method. Please specify GET or POST.这套校验机制直接解决了面试常问的“LLM 返回乱码怎么办”问题——答案不是重试而是用结构化约束把 LLM 当作一个不可靠但可引导的协作者。我在某次电商大促保障中将校验失败率从 12% 降到 0.3%关键就是第三层参数校验当 LLM 因高负载返回残缺 JSON 时系统不崩溃而是返回精准的缺失字段提示引导用户补充信息。3.3 第三步Action —— 工具调用的“沙箱化”与超时熔断pi-agent 的ToolExecutor类是真正的“执行守门员”。它不直接调用工具而是包裹了四层防护线程隔离每个工具调用都在独立线程池tool-executor-pool中执行避免慢 SQL 拖垮整个 Agent超时熔断默认 8 秒超时可 per-tool 配置超时后自动触发CircuitBreaker.open()后续 60 秒内相同工具调用直接返回CIRCUIT_OPEN错误输入净化对actionInput中的字符串字段如 SQL、URL做 XSS 过滤和长度截断防止恶意输入穿透结果归一化无论工具返回ListMapString, Object还是String统一序列化为 JSON 字符串存入Context确保下一轮 Observation 格式一致。最关键的实践技巧是超时时间的动态计算。我们不会给所有工具设固定 8 秒而是基于历史 P95 延迟动态调整// 伪代码从 Redis 读取最近 100 次 database-query 的耗时 double p95Latency redis.zscore(tool:db-query:latency, p95); int timeout (int) Math.max(3000, p95Latency * 1.5); // 至少 3 秒上限 1.5 倍 P95这样既避免一刀切超时导致误熔断又防止个别慢查询拖垮整体。上线后工具级熔断准确率从 68% 提升到 99.2%。3.4 第四步Observation —— 如何让 LLM 看懂“工具执行失败”的真实原因很多 Agent 框架把异常堆栈原样塞给 LLM结果 LLM 被NullPointerException或Connection refused搞晕反而生成更离谱的指令。pi-agent 的ErrorFormatter类做了人性化翻译SQLException: Timeout→Database query timed out. Try simplifying the SQL or checking network connectivity.HttpClientErrorException: 401 Unauthorized→Authentication failed for the API call. Verify your API key is correct and not expired.FileNotFoundException→The specified file path does not exist. Please check the path and try again.翻译规则不是硬编码而是用ErrorRule接口定义public interface ErrorRule { boolean matches(Throwable t); String format(Throwable t); } Bean public ErrorRule sqlTimeoutRule() { return t - t instanceof SQLException t.getMessage().contains(timeout); }业务方可以随时添加新规则比如对接内部风控系统时新增RiskBlockedException规则把风控拦截原因转成业务语言。注意事项不要在 Observation 中暴露敏感信息ErrorFormatter会自动过滤堆栈中的密码、token、IP 地址等字段用***替代。这是 Agent 安全的底线——哪怕 LLM 被攻破攻击者也拿不到数据库连接串。4. 实操过程从零启动一个可调试的 pi-agent 示例4.1 环境准备为什么推荐 JDK 17 Spring Boot 3.2 而非最新版pi-agent 的pom.xml明确指定java.version17/java.version spring-boot.version3.2.5/spring-boot.version这不是保守而是经过 17 个生产环境验证的黄金组合。JDK 17 的ThreadLocal内存泄漏修复JDK-8266312让Context对象在高并发下不再堆积Spring Boot 3.2 的RetryableTopic支持让工具调用失败后自动重试无需手写死循环。安装步骤极简下载 JDK 17推荐 Eclipse Temurin git clone https://github.com/pi-agent/pi-agent.gitcd pi-agent ./mvnw clean install -DskipTests跳过测试加速构建。提示别急着./mvnw spring-boot:run先配置application.ymlllm: provider: openai # 或 qwen/local openai: api-key: sk-xxx # 你的 OpenAI Key base-url: https://api.openai.com/v1 tool: database-query: enabled: true datasource: url: jdbc:h2:mem:testdb username: sa password:H2 内存数据库开箱即用避免你折腾 MySQL 安装。如果用 Qwen把provider改成qwen并配置qwen.api-key和qwen.base-url如 Ollama 的http://localhost:11434/api/chat。4.2 启动与调试如何用 IDE 直观看到 ReAct 循环的每一步在 IntelliJ IDEA 中右键PiAgentApplication.java→Debug PiAgentApplication在AgentExecutor.execute()方法第一行打断点发送测试请求curl -X POST http://localhost:8080/v1/execute \ -H Content-Type: application/json \ -d {userId:test,sessionId:sess-001,input:查张三的订单}你会看到调试窗口清晰展示四步流转Observation 阶段context.get(observation)显示增强后的上下文含时间戳和工具列表Thought 阶段llmClient.invoke(observation)返回的thought字段如I need to query the order table for user ZhangSanAction 阶段toolExecutor.execute(actionName, actionInput)调用DatabaseQueryToolactionInput是{sql: SELECT * FROM orders WHERE user_name ZhangSan}Observation 更新context.put(lastResult, [{id:1,product:phone}])为下一轮循环准备。实操心得在DatabaseQueryTool.execute()里故意抛出new RuntimeException(Simulated DB error)观察ErrorFormatter如何把异常转成用户友好的 Observation。这是理解错误处理机制最快的方式。4.3 关键配置项详解哪些参数决定 Agent 的“性格”pi-agent 的application.yml里藏着影响行为的 5 个关键开关配置项默认值作用调试建议agent.max-steps10单次会话最多执行步数防无限循环新手调成 3快速验证流程生产环境根据业务复杂度设 8~15llm.temperature0.3控制 LLM 创造性值越低越保守写 SQL 时设 0.1避免生成非法语法创意场景可提至 0.7tool.circuit-breaker.enabledtrue熔断开关生产必开本地调试可关方便复现超时场景memory.conversation-storein-memory记忆存储类型开发用in-memory生产切redis避免重启丢会话tool.http-request.timeout-ms5000HTTP 工具全局超时若调用内网服务可降为 2000调用第三方 API 建议 8000特别提醒agent.max-steps它不是性能参数而是安全围栏。某次灰度发布中因 LLM 模型微调失误Agent 在“查订单→导出 Excel→发邮件→查邮件状态→重发”间死循环。max-steps10让系统在第 10 步自动终止并返回MAX_STEPS_EXCEEDED错误避免资源耗尽。4.4 扩展一个自定义工具三步接入公司内部审批系统假设你要接入公司 OA 的“请假审批”接口步骤如下第一步定义工具类Component public class OaLeaveApprovalTool implements Tool { Value(${oa.api.url}) private String apiUrl; Override public String execute(Context context) { String userId context.get(userId); // 从 Observation 提取 String days context.get(days); // 调用 OA 接口此处用 RestTemplate String response restTemplate.postForObject( apiUrl /leave, new LeaveRequest(userId, days), String.class ); return Leave approval request submitted. Tracking ID: JsonPath.read(response, $.trackingId); } }第二步在application.yml中配置oa: api: url: https://oa.internal.company.com/api/v1 tool: oa-leave-approval: enabled: true第三步在 Observation 中声明工具修改ObservationBuilder在getAvailableTools()方法里添加if (toolRegistry.contains(oa-leave-approval)) { tools.add(oa-leave-approval: Submit leave approval request to OA system. Args: {\userId\: \user id\, \days\: \number of days\}); }完成现在 LLM 只要输出action: oa-leave-approval系统就会自动调用 OA 接口。整个过程不侵入核心逻辑符合开闭原则。5. 常见问题与排查技巧实录那些文档里不会写的坑5.1 问题速查表高频报错与根因定位报错信息可能根因排查命令解决方案InvalidThoughtException: Thought must start with I need toLLM 模型未对齐 ReAct 格式curl -s http://localhost:8080/actuator/health | jq .status检查llm.provider配置切换为qwen或微调 OpenAI 的 system promptToolNotFoundException: http-requesttool.http-request.enabledfalse或 Bean 未扫描./mvnw dependency:tree | grep tool-http在application.yml中设tool.http-request.enabledtrue确认HttpToolConfig类被Configuration注解CircuitBreakerOpenException: http-requestHTTP 工具连续失败触发熔断redis-cli get circuit:open:http-request清空 Redis 中的熔断状态redis-cli del circuit:open:http-request检查网络连通性JsonProcessingException: Cannot construct instance of java.time.LocalDateTimeLLM 返回的时间字符串格式不匹配curl -s http://localhost:8080/v1/execute -d {input:now}在application.yml中添加spring.jackson.date-formatyyyy-MM-dd HH:mm:ssNoClassDefFoundError: com/fasterxml/jackson/databind/JsonNodeJackson 版本冲突./mvnw dependency:tree -Dincludescom.fasterxml.jackson.core排除老版本exclusiongroupIdcom.fasterxml.jackson.core/groupIdartifactIdjackson-databind/artifactId/exclusion5.2 独家避坑技巧从 12 个生产事故中总结技巧 1用PostConstruct预热工具避免首请求超时某些工具如数据库连接池初始化耗时较长。在OaLeaveApprovalTool类中添加PostConstruct public void warmUp() { try { // 发送一次空请求触发连接建立 restTemplate.getForObject(https://oa.internal.company.com/health, String.class); } catch (Exception e) { log.warn(OA health check failed, will retry on first use); } }这样应用启动后立即建立连接首请求 RT 从 2.1s 降到 120ms。技巧 2为 LLM 输出加“防抖”层解决重复指令问题LLM 有时会连续两轮输出相同action如反复查同一张表。在AgentExecutor中加入private final SetString recentActions ConcurrentHashMap.newKeySet(); public AgentResponse execute(Context context) { String actionKey context.get(action) : context.get(actionInput); if (recentActions.contains(actionKey)) { throw new DuplicateActionException(Same action executed twice); } recentActions.add(actionKey); // ... 执行逻辑 }配合Scheduled(fixedDelay 30000)定时清理recentActions完美解决抖动。技巧 3用Timed监控每步耗时快速定位瓶颈在AgentExecutor.execute()上加 Micrometer 注解Timed(value agent.step.duration, histogram true) public AgentResponse execute(Context context) { ... }然后访问http://localhost:8080/actuator/metrics/agent.step.duration查看 P95 耗时分布。某次发现database-query步骤 P95 达 4.2s深入排查发现是 H2 数据库未建索引加索引后降至 80ms。技巧 4当 TS 前端报ts分片错误时检查web模块的 TypeScript 版本pi-agent/web使用 TypeScript 5.0若你的 VS Code 用 TS 4.9会出现TS2742: The inferred type of xxx cannot be named without a reference to yyy。解决方案在web/tsconfig.json中添加compilerOptions: {skipLibCheck: true}或升级 VS Code 的 TS 版本CtrlShiftP→TypeScript: Select TypeScript Version→Use Workspace Version。5.3 面试高频题实战拆解如何回答“Agent 安全怎么保障”当面试官问“Agent 安全”别只答“加鉴权”、“防注入”。结合 pi-agent 实践分三层回答第一层输入层防护Observation 构建时对用户输入做 HTML 转义和长度限制context.put(input, HtmlUtils.htmlEscape(input).substring(0, 500))所有工具参数如 SQL、URL走NotBlankSize(max2000)校验Spring Validation 自动拦截非法输入。第二层执行层隔离工具调用在独立线程池执行且线程池配置ThreadFactory设置setUncaughtExceptionHandler捕获未处理异常防止进程退出FileWriteTool严格限制路径前缀如只允许/tmp/用Paths.get(path).startsWith(allowedBase)校验杜绝路径遍历。第三层输出层净化LLM 返回的thought/action字段用Jsoup.clean()过滤 HTML 标签防止 XSS 渲染到前端AgentResponse序列化前用 Jackson 的JsonInclude(JsonInclude.Include.NON_NULL)避免返回 null 字段减少攻击面。最后补一句“安全不是功能而是贯穿 Observation→Thought→Action→Observation 每一步的肌肉记忆。”——这句话能让面试官立刻感受到你的工程深度。6. 性能与扩展性实测单机 800 QPS 下的稳定性数据6.1 压测环境与方法论我们用 JMeter 对 pi-agent 进行了 30 分钟持续压测硬件AWS EC2 c5.4xlarge16 vCPU, 32GB RAM软件OpenJDK 17.0.2, Spring Boot 3.2.5, H2 内存数据库场景模拟 200 个用户并发执行“查订单→导出 CSV→发邮件”三步流程指标重点关注agent.step.duration单步耗时、jvm.memory.used内存占用、tool.circuit-breaker.open熔断率。压测脚本关键参数elementProp nameHTTPsampler.Arguments elementTypeArguments collectionProp nameArguments.arguments elementProp nameinput elementTypeHTTPArgument stringProp nameHTTPArgument.valuequery order for user ${__RandomString(8,abcdefghijklmnopqrstuvwxyz)}/stringProp /elementProp /collectionProp /elementProp6.2 核心性能数据平均值指标数值说明平均 QPS812稳定维持在 800无请求堆积P95 单步耗时124ms其中 LLM 调用占 85ms工具执行占 39ms内存占用峰值2.1GBJVM 堆内存使用率 65%GC 频率 0.8 次/分钟熔断触发率0.02%仅 3 次因网络抖动触发 HTTP 工具熔断错误率0.07%全部为InvalidThoughtException由 LLM 格式错误导致注意P95 耗时 124ms 意味着 95% 的请求在 124ms 内完成单步。由于 ReAct 是串行循环三步流程的 P95 总耗时约为124ms × 3 372ms远低于用户可感知的 1 秒阈值。6.3 扩展性验证如何水平扩容应对百万级会话pi-agent 的设计天然支持水平扩展关键在三个无状态组件LLM 客户端OpenAiClient是无状态的所有请求通过RestTemplate发出可部署多实例前端用 Nginx 负载均衡工具执行器ToolExecutor依赖的DataSource、RestTemplate等均通过 Spring 管理实例化时自动注入无共享状态记忆存储RedisConversationStore将sessionId作为 Redis Key天然支持分布式访问。唯一需要共享的是工具配置中心。我们用 Spring Cloud Config Server 管理application.yml当新增tool.sms工具时在 Config Server 的pi-agent-dev.yml中添加配置所有 Agent 实例通过RefreshScope自动拉取新配置无需重启新工具立即生效。实测数据从