Luca Ju
技术笔记
首页
关于这个博客
个人主页
↗
Search
1
Java 实现Google 账号单点登录(OAuth 2.0)全流程解析
3,209 阅读
2
大模型“无脑复读”?3分钟搞懂原因+解决办法
1,776 阅读
3
Spring AI 无法获取大模型深度思考内容?解决方案来了
936 阅读
4
服务器遭遇 XMRig 挖矿程序入侵排查与清理全记录
519 阅读
5
EasyExcel 实战:导出带图片的 Excel 完整方案
516 阅读
AI大模型&智能体
Java 核心
框架与中间件
数据库技术
开发工具与效率
问题排查与踩坑记录
程序员成长与思考
前端
登录
Search
标签搜索
Spring AI
java虚拟机
JVM
Spring AI Alibaba
Java
保姆级教程
SpringBoot
Spring
MCP
WebFlux
龙虾
Agent Skills
大模型
OpenClaw
人工介入
Nginx
Ubuntu
Apache POI
自定义starter
Mybatis
Luca Ju
累计撰写
57
篇文章
累计收到
4
条评论
首页
个人主页 ↗
栏目
AI大模型&智能体
Java 核心
框架与中间件
数据库技术
开发工具与效率
问题排查与踩坑记录
程序员成长与思考
前端
页面
关于这个博客
搜索到
57
篇与
的结果
2026-09-19
Trellis:给 AI 编程加上一套工程流程(附贪吃蛇实战)
AI 写代码的速度确实很快,真正让人头疼的却不是“它会不会写”,而是“它下次还记不记得项目规范、上次做到哪、这次改动有没有经过检查”。Trellis 想解决的,就是这件事。前言这两年用了不少 AI Coding 工具,感受很明显:让 AI 生成一个页面、补一个接口,通常不难;难的是连续做几周以后,项目里的上下文开始散掉。比如这次让它改一个接口,得重新解释异常怎么处理;下次开新会话,又要再贴一遍目录结构、测试要求和提交规范。任务大一点时,需求、方案、代码、检查结果还会散落在聊天记录里。聊天一长,前面的结论基本等于没发生过。Trellis 不是另一个模型,也不是 IDE。它更像是一层放进项目里的工作流:把规范、任务和会话记录都落到文件里,再按当前阶段交给 AI。它不保证 AI 从此不犯错,但能把不少“本来应该提前想到”的事情变成固定步骤。一、Trellis 是什么Trellis 是面向 AI 编程的工程化工作流工具。初始化后,它会在项目中生成一套 .trellis/ 目录和对应平台的配置文件;之后无论用 Codex、Claude Code、Cursor 还是其他支持的平台,AI 都能按同一套任务、规范和检查流程协作。它主要解决 4 件事:规范不用反复贴:把项目约定放进 Spec,AI 在实现和检查时按需读取;需求不只存在聊天里:一个任务有自己的 PRD、设计、实施计划和研究记录;会话切换不容易失忆:重要过程写入工作日志,后续会话可以接着做;实现后有检查环节:代码不是“生成完就结束”,还要回到任务要求、规范和测试上过一遍。官方把它称为 AI coding 的工程框架。这个描述不算夸张:它关心的不是帮你多写几行代码,而是让 AI 的每一次改动更接近正常开发流程。官方仓库 和 npm 包 都可以直接查看最新版本与文档。二、.trellis 目录里有什么初始化后,项目里最核心的是下面几部分:.trellis/ ├── workflow.md # 工作流:规划、实现、检查、收尾 ├── config.yaml # 项目级配置 ├── spec/ # 项目规范 ├── tasks/ # 任务与任务产物 ├── workspace/ # 开发者会话日志 ├── scripts/ # 本地任务与上下文运行脚本 └── .runtime/ # 会话级运行状态(自动生成)1. workflow.md:告诉 AI 现在该做什么这里定义的是任务节奏,通常分为:Plan → Execute → Finish规划阶段先把需求说清楚;实现阶段再写代码和检查;收尾阶段补充规范、提交并归档任务。听起来有点“流程化”,但在 AI 编程里很有必要。因为一句“帮我做个用户管理”包含的东西太多了:要不要分页?权限怎么分?失败怎么提示?有没有现成接口?不先收敛需求,AI 很容易按自己的理解补全,最后看起来什么都有,实际并不贴合项目。2. spec/:项目规范的长期存放处Spec 不是一份泛泛的编码建议,而是项目真实约定。例如:spec/ ├── frontend/ │ ├── directory-structure.md │ ├── state-management.md │ └── quality-guidelines.md ├── backend/ │ ├── database-guidelines.md │ ├── error-handling.md │ └── logging-guidelines.md └── guides/这里可以记录组件放哪里、接口异常怎么返回、数据库迁移怎么写、哪些逻辑必须有测试。以后创建任务时,只把和当前任务相关的 Spec 注入给 AI,不需要让它每次读一大坨规则。我比较认同这一点:规则应该跟着代码仓库走,而不是只躺在某个同事的提示词收藏夹里。3. tasks/:一件事从需求到完成的完整记录一个任务目录通常长这样:tasks/09-18-snake-game/ ├── task.json # 状态、负责人、优先级等元数据 ├── prd.md # 需求与验收标准 ├── design.md # 技术设计(复杂任务才需要) ├── implement.md # 实施步骤与验证方式 ├── implement.jsonl # 实现代理要读的规范/研究清单 ├── check.jsonl # 检查代理要读的规范/研究清单 └── research/ # 调研记录其中最实用的是 prd.md。它不是产品文档的摆设,而是后面验收代码的依据:哪些功能必须做、哪些明确不做、做到什么程度才算完成,都应该写在这里。4. workspace/:给跨会话留一份可追溯的记录聊天记录会压缩、会丢上下文,工作日志不会。workspace/ 按开发者记录会话摘要、关键决定和提交信息。下次继续一个任务时,先看任务文件,再看日志补背景,基本不会出现“昨天做到哪了”的问题。三、安装 TrellisTrellis 的前置要求是 Node.js 20+ 和 Python 3.9+。准备好后,先全局安装:npm install -g @mindfoldhq/trellis@latest进入需要接入的项目目录,执行初始化:cd your-project trellis init -u your-nameyour-name 会成为开发者身份,并创建对应的个人工作区。如果只使用特定 AI 工具,可以显式指定。例如我现在用 Codex:trellis init -u luca --codex也可以同时配置多个平台:trellis init -u luca --claude --cursor --codex初始化后,先别急着让 AI 开始写业务代码。建议先把 .trellis/spec/ 里的模板补成项目真实规范:目录怎么划分、异常怎么处理、前端状态放哪里、测试最低要求是什么。Spec 越贴近现状,后面得到的代码就越像项目原本的代码。提示:Trellis 的 CLI 和项目内模板是两套版本。升级 CLI 后,通常还需要在项目目录执行 trellis update 同步生成文件;如果提示迁移要求,再按提示执行 trellis update --migrate。具体以官方安装文档为准。四、日常怎么用日常使用不需要把每个命令背下来。最常见的流程就是下面这 4 步。1. 先说清楚要做什么直接对 AI 描述需求,例如:给管理后台增加一个导出功能,支持按当前筛选条件导出 Excel。Trellis 会引导 AI 进入规划:先确定筛选条件从哪里来、导出列有哪些、数据量大时怎么处理、哪些边界属于本次范围。结论会写入 prd.md,而不是只停留在对话里。2. 规划完成后再开始实现复杂任务会补充 design.md 和 implement.md。设计文档负责回答“怎么做、为什么这样做”,实施计划负责列出改哪些文件、怎么验证、哪里可以回退。任务准备好后,Trellis 会把相关 Spec、PRD 和研究资料交给实现代理。实现代理只负责在范围内写代码,不顺手扩大需求,也不替主会话做提交。3. 让检查代理复核实现结束后,检查代理会重新对照 PRD 和 Spec,跑 lint、类型检查和测试。能机械修复的地方直接修,例如缺失导入、类型问题、明显的边界遗漏;涉及产品取舍的地方则会留下问题,不会擅自改需求。这个环节很适合发现“功能看似正常,但遗漏了验收条件”的问题。4. 收尾时归档和沉淀任务结束前,检查这次有没有产生值得写进 Spec 的经验。例如某个接口的错误码约定、一个容易踩的并发问题、某类组件的固定写法。确认无误后,再完成提交、归档任务和记录会话。下一次面对类似需求,AI 就能从现有 Spec 和任务历史里拿到更准确的上下文。五、小实战:用 Trellis 做一个贪吃蛇下面用一个简单但完整的例子,看看 Trellis 到底帮了什么。需求很直接:做一个能在浏览器中直接打开的网页版贪吃蛇,只支持桌面键盘操作。1. 先把范围钉住一开始如果只说“做个贪吃蛇”,AI 很可能顺手加移动端按键、难度选择、排行榜甚至音效。它们不一定错,但不一定是这次想要的。这次在 PRD 里明确写了:需要:方向键控制、计分、随机食物、撞墙/撞自己结束、重新开始。 不需要:后端、排行榜、存档、联网、移动端触摸按钮。同时给出可观察的验收标准:食物不能生成在蛇身上;吃到食物后长度和分数要增加;撞墙或撞到自己要停止;重新开始不能残留旧定时器。这一步花不了多久,却能避免后面返工。2. 设计保持简单因为没有现成前端工程,也不需要后端,最终只新增 3 个文件:index.html # 页面结构、Canvas 棋盘、状态和重开按钮 styles.css # 桌面端样式 game.js # 游戏状态、输入、循环、碰撞和绘制游戏状态集中在 game.js:蛇身数组、当前方向、下一步方向、食物坐标、分数、运行状态和计时器。一次 tick 的过程也很直白:键盘输入 → 更新下一步方向 → 计算新蛇头 → 判断撞墙/撞身 → 吃食物或移动蛇身 → 重绘画面这里有两个小细节值得提前写在设计里:方向键会触发浏览器滚动,需要阻止默认行为;重开前必须清理旧的 setInterval,否则会出现两套循环同时跑,蛇移动速度越来越快。3. 实现后做一次真正的检查实现代理完成页面和游戏逻辑后,检查代理重点验证了几个容易被忽略的分支:连续按方向键时,不能绕过“禁止直接反向”;食物只从未被蛇占用的格子里选择;蛇填满棋盘时要结束并显示胜利状态;碰撞结束和点击重开都要正确清理定时器;浏览器不支持 Canvas 时,页面不能直接报错。最后还补了 Canvas 不可用时的降级提示和禁用按钮样式。这类问题并不复杂,但如果没有一个独立检查环节,很容易因为“主流程能玩”就被忽略。4、效果图总结Trellis 适合的不是“让 AI 代替开发”,而是让 AI 的开发过程更可控。如果只是临时写一个脚本,完整流程可能显得有点重;但只要任务开始涉及多人协作、跨会话、项目规范或长期维护,Trellis 的价值会慢慢显现出来:需求有落点,规范能复用,检查有依据,下一次也不用从零解释。我觉得可以先从一个小项目试起:初始化 Trellis,补两三条真正会用到的 Spec,然后让 AI 完成一个完整的小功能。等你经历过一次“需求—实现—检查—归档”的闭环,再决定要不要把它放进日常开发流程。参考资料:Trellis GitHub 仓库Trellis npm 包安装与首个任务文档
2026年09月19日
28 阅读
0 评论
1 点赞
2026-08-28
从 Prompt Engineering 到 Harness Engineering:Agent 开发为什么越来越像系统工程?
前言:为什么现在又开始讨论 Context Engineering 和 Harness Engineering?刚开始接触大模型开发的时候,我们经常会遇到一个问题:这个 Prompt 到底应该怎么写?于是我们开始研究 Prompt Engineering:System Prompt 怎么设计?Role 应该怎么定义?Few-shot 要不要加?输出格式怎么约束?怎么减少模型幻觉?随着 Agent 应用越来越复杂,我们很快会发现:很多问题根本不是 Prompt 写得不好。比如:Prompt 已经写得很清楚了, 但是模型不知道数据库里的实时数据。 Prompt 已经写得很清楚了, 但是历史对话太长,模型抓不到重点。 Prompt 已经写得很清楚了, 但是工具返回的数据太多,模型反而不知道该看什么。于是,我们开始关注另外一个问题:到底应该给模型什么上下文?这就是 Context Engineering。但是继续往前做 Agent,又会遇到更加棘手的问题:模型知道该做什么; 也拿到了正确的信息; 但是: 工具调用失败怎么办? 任务执行到一半怎么办? 状态怎么保存? 结果怎么验证? 失败之后怎么重试? Agent 能访问哪些资源?这时候你会发现:Prompt 和 Context 解决的只是 Agent 的一部分问题。真正要让 Agent 稳定完成任务,还需要一个完整的运行环境。于是又出现了 Harness Engineering。所以,如果把这几个概念放到一起看,我更倾向于把它理解成一个逐渐演进的过程:Prompt Engineering ↓ 解决“模型应该怎么做” ↓ Context Engineering ↓ 解决“模型需要知道什么” ↓ Harness Engineering ↓ 解决“模型如何可靠地完成任务”本文就从 Agent 开发的角度,聊聊这三个概念之间到底是什么关系。1、Prompt Engineering:先解决“怎么告诉模型做什么”Prompt Engineering,也就是我们最熟悉的提示词工程。在最早的大模型应用中,系统通常非常简单:用户输入 ↓ Prompt ↓ LLM ↓ 回答例如:你是一名发动机选型专家。 请根据用户的需求, 从候选发动机中选择最符合要求的型号。 要求: 1. 优先满足用户明确提出的条件 2. 不允许虚构不存在的配置 3. 如果条件无法满足,需要明确告诉用户 4. 最终给出推荐型号以及推荐理由这里我们实际上是在告诉模型:你是谁你要做什么应该遵循什么规则最后应该输出什么这就是 Prompt Engineering 最核心的工作。可以简单理解成:Prompt Engineering 解决的是模型的“行为规范”。2、Prompt Engineering 为什么越来越不够用了?假设现在我们真的做一个发动机智能选配 Agent。用户说:帮我选择一个功率 150kW 以上, 排量不超过 2.5L 的发动机。Prompt 可以告诉模型:必须满足用户提出的条件。但是问题来了:模型怎么知道现在有哪些发动机?难道我们把数据库中的所有发动机都写进 Prompt?比如:发动机 A: 功率:120kW 排量:2.0L 价格:8000 发动机 B: 功率:150kW 排量:2.5L 价格:10000 发动机 C: 功率:180kW 排量:2.5L 价格:12000 ......如果数据量只有几十条,还可以勉强接受。但是实际业务系统可能有:发动机 + 零部件 + 配置参数 + 价格 + 库存 + 车型 + 客户历史选择数据量很容易达到几万甚至几十万条。这时候 Prompt 已经不是 Prompt 了。而变成了一个:巨大的数据容器。所以问题开始发生变化:以前: Prompt 怎么写? 现在: 模型真正需要哪些信息?这就是 Context Engineering 出现的原因。3、Context Engineering:解决“模型需要知道什么”Context Engineering 可以简单理解成:围绕当前任务,为模型动态构建高质量上下文。这里的关键词其实是:动态。不是把所有数据都提前塞给模型,而是根据当前任务,找到模型真正需要的信息。整个过程可能是:用户请求 ↓ 理解用户需求 ↓ 确定需要哪些信息 ↓ 查询数据库 / 知识库 / API ↓ 筛选相关数据 ↓ 整理 / 压缩 ↓ 构建 Context ↓ 发送给 LLM比如:用户: 我需要一个功率 150kW 以上、 排量不超过 2.5L 的发动机。Agent 首先解析出:power >= 150kW displacement <= 2.5L然后去查询数据库:SELECT * FROM engine WHERE power >= 150 AND displacement <= 2.5;数据库返回:A:150kW / 2.5L / 10000元 B:160kW / 2.3L / 12000元 C:180kW / 2.5L / 9000元然后把这些数据组织成模型需要的 Context:用户需求: 功率 >= 150kW 排量 <= 2.5L 候选发动机: A: 功率:150kW 排量:2.5L 价格:10000元 B: 功率:160kW 排量:2.3L 价格:12000元 C: 功率:180kW 排量:2.5L 价格:9000元这时候 LLM 才真正开始发挥它擅长的能力:基于已经获取到的信息进行理解、推理和解释。4、Context Engineering 和 Prompt Engineering 到底有什么区别?这两个概念其实特别容易混。我觉得可以用一句话来区分:Prompt Engineering 解决“怎么告诉模型做事”,Context Engineering 解决“给模型什么信息做事”。例如:Prompt你是一名发动机选型专家。 请选择满足用户要求的发动机。 不得虚构数据。这是:模型应该怎么做Context用户要求: 功率 >= 150kW 候选发动机: A:120kW B:150kW C:180kW这是:模型需要知道什么因此二者并不是替代关系。而是:Prompt + Context ↓ LLMPrompt 负责规则和行为。Context 负责事实和状态。5、Context Engineering 不等于 RAG这里也有一个比较常见的误区:Context Engineering = RAG?其实不是。RAG 只是 Context Engineering 的一种实现方式。Context 的来源可以非常多: Context │ ┌────────────┼────────────┐ ↓ ↓ ↓ RAG Database API │ │ │ ↓ ↓ ↓ 知识文档 业务数据 实时数据 ┌────────────┼────────────┐ ↓ ↓ ↓ Conversation Memory Tool Result │ │ │ ↓ ↓ ↓ 历史对话 用户记忆 工具执行结果所以在实际 Agent 项目中:Context Engineering │ ├── RAG ├── Database Query ├── API ├── Tool Result ├── Conversation History ├── Memory ├── Task State └── Runtime Information这些都属于 Context Engineering 的范畴。6、Context Engineering 的核心:不要什么都给模型这里其实有一个非常重要的思想:Context 不是越多越好。以前我们经常有一种思维:模型不知道? → 给它更多信息。但在 Agent 中,往往不是这样。信息太多反而会带来:Token 消耗增加上下文噪声增加模型注意力分散无关信息干扰决策Context Window 被快速消耗所以更合理的方式是:不是: 把所有信息给模型 而是: 找到当前任务真正需要的信息也就是:Just-in-Time Context。什么时候需要什么信息,就什么时候获取。7、为什么 Agent 做到后面,又会出现 Harness Engineering?假设现在 Context 已经解决了。模型已经知道:用户需要什么 有哪些候选发动机 当前库存是多少 价格是多少 历史对话是什么但是 Agent 还需要完成:查询数据库 ↓ 调用库存接口 ↓ 调用价格接口 ↓ 校验配置 ↓ 生成结果这时候新的问题来了。比如:库存接口调用失败怎么办? 数据库连接超时怎么办? 模型选出的配置不符合业务规则怎么办? 工具返回的数据格式错误怎么办? Agent 执行到一半用户退出怎么办? 任务执行到一半 Context 太长怎么办?这些问题已经很难通过 Prompt 解决了。因为:它们属于系统运行问题,而不是模型指令问题。于是,我们开始需要一个更完整的东西:Harness。8、Harness Engineering:给 Agent 构建一个“工作环境”Harness 这个词本身可以理解成“驾驭、控制、约束”。在 Agent 场景下,可以把 Harness 简单理解为:围绕 LLM 构建的一套 Agent 运行环境。它负责让模型不仅“会思考”,而且能够:获取信息 ↓ 调用工具 ↓ 执行任务 ↓ 观察结果 ↓ 修改计划 ↓ 继续执行 ↓ 验证结果 ↓ 失败恢复因此一个完整的 Agent 已经不再是:Prompt ↓ LLM ↓ Answer而更像: Agent Harness │ ┌──────────────┼──────────────┐ ↓ ↓ ↓ Context Tools State │ │ │ └──────────────┼──────────────┘ ↓ LLM ↓ Action ↓ Environment ↓ Result ↓ Context Update ↓ LLM9、Harness Engineering 主要解决哪些问题?如果把 Harness 拆开,其实就是我们做 Agent 时经常遇到的那些工程问题。9.1 Tool:Agent 能做什么?例如:query_engine() query_inventory() query_price() validate_config()模型负责决定:“我现在需要调用哪个工具?”Harness 负责:“这个工具到底怎么执行?”9.2 State:Agent 现在做到哪里了?例如:{ "task": "发动机选型", "status": "validating", "selected_engine": "A", "inventory_checked": true, "price_checked": true }如果没有状态管理,复杂 Agent 很容易:做过的事情重复做 ↓ 上下文越来越混乱 ↓ Agent 开始“失忆”所以 State Management 也是 Harness 的重要组成部分。9.3 Execution Loop:Agent 怎么持续工作?一个典型 Agent Loop:Think ↓ Act ↓ Observe ↓ Think ↓ Act ↓ Observe ↓ ...也就是:LLM ↓ Tool Call ↓ Tool Result ↓ LLM ↓ Tool Call ↓ Tool ResultHarness 负责把这个循环真正跑起来。10、Validation:为什么 Agent 不能相信自己?这是我认为 Harness Engineering 中非常重要的一点。例如 Agent 修改代码后说:代码已经修复。这句话其实没有意义。真正应该做的是:修改代码 ↓ 运行测试 ↓ 测试通过? ├── 否 → 获取错误信息 │ ↓ │ 修改代码 │ ↓ │ 再次测试 │ └── 是 → 任务完成也就是说:Agent 负责提出行动,Harness 负责验证行动结果。这也是为什么现在的 Coding Agent 越来越像一个完整的软件工程系统。11、一个实际的 Coding Agent 就非常典型比如我们让 Agent:帮我修复这个 Bug。一个真正能够工作的 Coding Agent,需要:读取代码 ↓ 搜索相关文件 ↓ 理解代码 ↓ 修改代码 ↓ 执行测试 ↓ 读取测试结果 ↓ 分析错误 ↓ 再次修改 ↓ 再次测试 ↓ Git Diff ↓ 最终验证这里:Prompt Engineering告诉 Agent:你是一个优秀的软件工程师。 修改代码前先理解现有代码。 不要修改无关文件。 修改后必须运行测试。解决:应该怎么工作。Context Engineering给 Agent:项目结构 相关代码 README CLAUDE.md / AGENTS.md 历史修改 测试结果 Git Diff解决:应该知道什么。Harness Engineering提供:File System Shell Git Test Runner Search Sandbox Permission State Retry Validation解决:应该怎么真正把事情做完。12、三者之间到底是什么关系?现在就可以比较清晰地理解三者了。 Prompt EngineeringContext EngineeringHarness Engineering核心问题怎么告诉模型?给模型什么信息?怎么让 Agent 完成任务?主要对象PromptContextAgent Runtime关注重点指令、规则、格式数据、记忆、状态、检索工具、执行、状态、验证解决问题行为信息能力与可靠性典型手段System Prompt、Few-shotRAG、DB、Memory、Tool ResultTool、Loop、Sandbox、Retry、Validation类比工作说明书工作资料整个工作环境所以可以用三个问题快速判断:模型不知道“应该怎么做” ↓ Prompt Engineering 模型不知道“完成任务需要什么信息” ↓ Context Engineering 模型知道怎么做,也知道需要什么, 但就是无法稳定完成任务 ↓ Harness Engineering13、为什么我认为这是 Agent 开发的一条演进路线?回过头来看,会发现这三个概念并不是突然出现的。它其实对应了 AI 应用开发的三个阶段。第一阶段:Prompt Engineering最开始:User ↓ Prompt ↓ LLM ↓ Answer重点是:如何让模型回答得更好。第二阶段:Context Engineering后来:User ↓ Retrieve / Query / Memory ↓ Context ↓ LLM ↓ Answer重点变成:如何让模型获得正确的信息。第三阶段:Harness Engineering再往后: Agent │ ┌──────────┼──────────┐ ↓ ↓ ↓ Context Tools State │ │ │ └──────────┼──────────┘ ↓ LLM ↓ Action ↓ Environment ↓ Observation ↓ Validation ↓ Continue重点变成:如何让模型在真实环境中持续完成复杂任务。所以我更愿意把它看成:Prompt Engineering ↓ Context Engineering ↓ Harness Engineering ↓ Agent Engineering14、那么现在做 Agent,到底应该重点学什么?如果是刚开始做 AI 应用,我认为 Prompt Engineering 依然值得学习。因为:Prompt仍然是模型行为控制的基础。但是如果已经进入 Agent 开发阶段,仅仅研究 Prompt 的收益就会越来越低。这时候更应该把精力放在:Context ├── 如何获取 ├── 如何筛选 ├── 如何压缩 ├── 如何组织 └── 如何动态更新 Tools ├── 如何设计 ├── 如何调用 ├── 如何处理异常 └── 如何控制权限 State ├── Task State ├── Conversation State └── Agent Memory Execution ├── Agent Loop ├── Retry ├── Timeout └── Recovery Validation ├── Rule Validation ├── Tool Validation └── Result Validation也就是说:Agent 开发正在逐渐从“Prompt 技巧”转向“系统工程能力”。15、最后总结如果让我用最简单的方式总结这三个概念:Prompt Engineering告诉模型应该怎么做。Context Engineering告诉模型完成任务需要知道什么。Harness Engineering给模型提供完成任务所需要的工具、环境和运行机制。三者组合起来就是: Agent │ ┌────────┼────────┐ ↓ ↓ ↓ Prompt Context Harness │ │ │ ↓ ↓ ↓ 怎么做 知道什么 能做什么 │ │ │ └────────┼────────┘ ↓ LLM ↓ 完成真实任务所以,Agent 真正的工程能力,并不是单纯把一个 LLM API 调通,也不是把 Prompt 写得越来越长。而是逐渐建立这样一套完整的系统:用 Prompt 定义规则,用 Context 提供信息,用 Harness 提供能力和运行环境,再通过 Validation 保证最终结果可靠。这也是为什么现在越来越多的 Agent 产品,看起来已经不像传统意义上的“AI 聊天机器人”,而更像一个围绕 LLM 构建的完整软件系统。LLM 只是其中的大脑。真正决定 Agent 能不能把事情做好的,是围绕这个“大脑”构建起来的:Prompt + Context + Tools + State + Execution + Validation + Environment而这可能才是 Agent Engineering 真正开始走向工程化的地方。
2026年08月28日
37 阅读
0 评论
1 点赞
2026-05-26
做共享目录实时同步,踩过这些坑
项目背景:我们有一个知识库平台,之前都是用户在平台上上传文档,然后执行解析向量数据库,用于后续执行RAG现在客户希望可以文档上传到共享目录,系统就可以自动把这些文件同步到知识库。问题:为什么不用轮询?最直白的方案是定时扫目录,对比前后两次的文件列表差异。这个方案写起来简单,但有几个硬伤:延迟高:扫描周期设 5 分钟,用户上传后平均要等 2.5 分钟才能感知到IO 浪费:目录下文件多的时候,每次全量扫一遍磁盘开销不小无法感知"修改":如果只是文件内容变了但大小没变,很难准确判断所以决定用 java.nio.file.WatchService 做事件驱动监听。WatchService 的基本用法WatchService 本质上是对操作系统底层文件事件通知的封装。Linux 下用的是 inotify,Windows 下是 ReadDirectoryChangesW。注册一个目录的监听很简单:WatchService watchService = FileSystems.getDefault().newWatchService(); Path dir = Paths.get("/share"); WatchKey key = dir.register(watchService, ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE);然后在一个独立线程里 take() 阻塞等待事件:WatchKey key = watchService.take(); for (WatchEvent<?> event : key.pollEvents()) { // 处理事件 } key.reset(); // 必须 reset,否则后续事件收不到坑一:WatchService 不会递归监听子目录这是最容易被忽略的一点。dir.register(...) 只监听这一层目录,子目录下的文件变更完全收不到。我们的共享目录结构是 /share/知识库名/文件,层级不固定,用户还可能随时新建子目录。所以必须自己实现递归注册。方案是在启动时把整个目录树 walk 一遍,每个子目录都注册上:Files.walkFileTree(root, EnumSet.of(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE, new SimpleFileVisitor<>() { @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { register(dir); // 每个目录都注册监听 return FileVisitResult.CONTINUE; } });同时监听 ENTRY_CREATE 事件,当发现新建的是目录时,立刻给它补上注册:if (kind == ENTRY_CREATE && Files.isDirectory(child)) { registerAll(child); }坑二:目录创建时,里面的文件可能"漏事件"这是个很隐蔽的竞态条件。假设用户通过 cp -r 拷贝一整个目录进来,操作系统产生的事件顺序可能是:ENTRY_CREATE —— 新目录 /share/A/BENTRY_CREATE —— 目录下的文件 file1.txtENTRY_CREATE —— 目录下的文件 file2.txt但如果我们的处理逻辑在第 1 步给 /share/A/B 注册监听时稍微慢了一点,第 2、3 步的事件可能已经被操作系统发出,而我们还没注册上,于是漏掉了。解决办法是在注册新目录后,主动扫一遍目录里已有的文件,补发创建事件:if (Files.isDirectory(child)) { registerAll(child); emitCreateForExistingFiles(child); // 兜底补发 }private void emitCreateForExistingFiles(Path dir) { try (Stream<Path> pathStream = Files.walk(dir)) { pathStream.filter(Files::isRegularFile).forEach(eventHandler::onCreate); } }坑三:事件防抖文件写入往往不是原子操作。比如用户拷贝一个大文件,或者用户频繁修改文件,操作系统可能产生一连串的 MODIFY 事件。如果每个事件都触发一次上传,既浪费资源又容易出错。我们在事件处理器里加了基于时间窗口的去抖:String debounceKey = eventType + "|" + fullPath; long now = System.currentTimeMillis(); Long last = debounceMap.get(debounceKey); if (last != null && now - last < windowMillis) { return; // 同一个文件同类型事件,5 秒内只处理一次 } debounceMap.put(debounceKey, now);这里用 ConcurrentHashMap 保证线程安全,key 的设计是"事件类型 + 文件路径",这样创建和修改事件互不干扰。坑四:应用重启期间的事件丢失WatchService 是内存态的,应用一重启,期间发生的所有文件变更都感知不到了。这对于生产环境是不可接受的。我们的补偿方案是快照比对:每次补偿扫描时,把当前目录下所有文件的路径、大小、最后修改时间文件hash记录下来,存到 db下次扫描时加载上次的快照,和当前状态做 diff新出现的文件 → 补发 CREATE大小或修改时间变化的 → 补发 MODIFY快照里有但现在没了的 → 补发 DELETE快照存 MySQL 而不是本地文件,是为了在集群部署时也能共享状态(虽然当前是单实例,但预留了扩展空间)。补偿扫描有两个触发时机:启动时立即执行一次:覆盖重启期间的漏事件定时执行:默认 5 分钟一次,作为兜底@Scheduled(fixedDelayString = "${doc.share-sync.reconcile-delay-ms:300000}") public void scheduledReconcile() { reconcile("scheduled"); }整体架构整个同步模块拆成了四个组件,职责很清晰:组件职责ShareDirWatcher基于 WatchService 的实时监听,递归注册子目录ShareFileEventHandler事件去抖、路径解析(从文件路径反查知识库ID)、文件处理ShareDirReconcileJob停机补偿扫描,快照比对补发事件ShareFileSnapshotStore快照的持久化存储(MySQL)配置也做了开关控制,方便分阶段上线:doc: share-sync: enabled: true dry-run: true # 第一阶段只打日志,不真写 root-dir: /share settle-seconds: 5 # 防抖窗口 reconcile-delay-ms: 300000一些没写的细节OVERFLOW 事件:WatchService 可能因事件队列满而丢失事件,此时会收到一个 OVERFLOW 事件。我们的处理是直接跳过,依赖定时补偿扫描来修复。删除事件的路径:ENTRY_DELETE 事件发生时,文件已经没了,ev.context() 只能拿到文件名,不能判断它是不是目录。所以删除事件统一走 onDelete,由下游根据数据库记录判断之前是什么。重复上传风险: 监听程序和定时补偿程序同时发现新创建了文件,会出现重复上传的问题。处理方案是添加数据库唯一索引,直接从数据库兜底总结WatchService 来做文件监听是我目前实现的方案,递归注册、新目录兜底扫描、事件去抖、停机补偿,这几块缺一不可。第一阶段先跑 dry-run,把日志打全、路径解析逻辑验证清楚,第二阶段再放开真实写入,这样上线心里比较有底。
2026年05月26日
54 阅读
0 评论
2 点赞
2026-05-15
一次 OOM 线上排查实录
大家好,今天分享一次真实的线上 OOM 排查过程,踩坑 Druid 连接池的经典内存泄漏问题,以及完整的解决思路。一、问题现场:线上内存飙高,OOM 报警某天,线上老项目突然收到服务器内存使用率持续飙高的报警,紧接着应用直接抛出 OOM 错误,服务崩溃。紧急拉取了堆 Dump 文件,用 JProfiler 打开后,直接看到了内存占用的元凶:大量 com.alibaba.druid.proxy.jdbc 相关对象堆积堆中最大的单个对象是一个 char[],大小超过 500MB,存储的正是项目中执行的 SQL 字符串结合项目业务场景,初步判断是数据库操作相关的内存泄漏,定位方向直接锁定了代码中的 SQL 操作和 Druid 连接池配置。二、根因定位:双重问题叠加导致的灾难顺着堆 Dump 里的 SQL 文本,我直接定位到了业务代码,发现这次 OOM 是两个问题叠加导致的。1. 业务代码:SQL 拼接逻辑导致大对象堆积这是一个老项目,当年的开发同学已经离职了,代码里存在这样的逻辑:单条 INSERT 语句中,通过循环拼接 SQL 字符串,一次性插入大量数据当数据量较大时,拼接后的 SQL 字符串会变得非常大,生成的 char[] 对象直接占用几百 MB 内存这些大字符串被线程栈引用,短时间内无法被 GC 回收,直接推高了内存水位2. 框架层面:Druid 1.1.22 版本的经典 SQL 缓存泄漏堆 Dump 中大量的 Druid 对象,指向了一个更致命的问题:Druid 连接池的 SQL 统计缓存。项目使用的 Druid 版本是 1.1.22,这个版本存在一个广为人知的问题:SQL 统计功能会无限制缓存所有执行过的 SQL 字符串,无法自动清理项目中拼接的大量不同 SQL,会被 Druid 全部缓存到 sqlStatMap 中,这些对象会一直持有 SQL 字符串的引用,导致它们无法被 GC 回收随着服务运行时间增长,缓存的 SQL 越来越多,内存只会涨不会跌,最终撑满堆内存,触发 OOM三、解决方案:两步走彻底根治问题针对这两个问题,我们采用了业务+框架双管齐下的修复方案,从根源解决内存泄漏。第一步:优化 Druid 配置,掐断缓存泄漏直接修改项目的 Druid 配置,关闭无限制的 SQL 统计,同时限制缓存大小,避免内存无限增长。方案 A:彻底关闭 SQL 统计(推荐,零泄漏风险)spring: datasource: druid: filter: stat: enabled: false # 关闭导致内存泄漏的SQL统计 web-stat-filter: enabled: false # 关闭Web统计,减少额外内存占用方案 B:保留监控,限制缓存大小(折中方案)如果业务必须保留 SQL 监控,可以通过配置限制缓存的 SQL 数量,避免无限增长:spring: datasource: druid: filter: stat: enabled: true max-stat-count: 200 # 限制最多缓存200条SQL,超出自动淘汰第二步:重构业务代码,替换 SQL 拼接为批量插入修改原有的 SQL 拼接逻辑,改为标准的批量插入方式,既避免了超大 SQL 字符串的生成,也提升了数据库写入性能。改造前(问题代码)// 循环拼接SQL,生成超大字符串 StringBuilder sql = new StringBuilder("INSERT INTO t_invoice (col1, col2) VALUES "); for (Invoice invoice : list) { sql.append("(?, ?),"); } jdbcTemplate.update(sql.toString(), params);改造后(批量插入)// 使用JdbcTemplate批量插入,避免生成超大SQL字符串 String sql = "INSERT INTO t_invoice (col1, col2) VALUES (?, ?)"; jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() { @Override public void setValues(PreparedStatement ps, int i) throws SQLException { ps.setString(1, list.get(i).getCol1()); ps.setString(2, list.get(i).getCol2()); } @Override public int getBatchSize() { return list.size(); } });四、效果验证与后续优化改造完成后,我们重新上线服务并进行了压测验证:内存曲线恢复平稳,不再出现持续飙高的情况堆 Dump 中 Druid 相关对象和大 char[] 基本消失数据库写入性能也有明显提升,单批次插入耗时降低了 40%额外优化建议对于老项目,建议升级 Druid 到最新稳定版(如 1.2.20+),修复了大量已知的内存泄漏问题批量插入时,建议设置合理的批次大小(如每批 100-500 条),避免单次操作过大导致数据库压力上线前务必进行压测,通过 JProfiler 或 Arthas 观察内存变化,提前发现潜在问题五、踩坑总结这次 OOM 排查给了我两个深刻的教训:老项目的依赖版本一定要关注:Druid 1.1.22 这个版本的 SQL 缓存泄漏问题非常普遍,很多线上 OOM 都源于此,升级或关闭统计是最直接的解决方式。业务代码的 SQL 拼接是隐形杀手:不仅容易导致 SQL 注入,还会生成超大对象,配合框架的缓存机制,很容易引发内存泄漏。批量插入是更安全、更高效的替代方案。希望这次分享能帮到遇到同样问题的朋友,如果你也遇到了 Druid 相关的内存问题,欢迎在评论区交流讨论~
2026年05月15日
51 阅读
0 评论
2 点赞
2026-04-30
DeepAgents 人工介入实战|LangGraph 实现 Agent 高危工具人工审批
之前一篇文章里,我使用 Spring AI Alibaba 演示了智能体执行过程中的人工介入能力。那篇文章的核心思路是:当 Agent 准备执行某些高风险动作时,不要让它直接执行,而是先暂停下来,把待执行动作交给人工审批,审批通过后再继续执行。感兴趣的小伙伴可以通过下面链接回顾一下:https://www.lucaju.cn/index.php/archives/165/这篇文章换一个技术栈,使用 Python 版本来实现同样的能力。本次示例基于:LangChain:负责模型和工具抽象LangGraph:负责执行状态、检查点和恢复执行DeepAgents:负责创建支持工具调用和中断审批的 Agent通义千问兼容 OpenAI API:作为底层大模型一、为什么 Agent 需要人工介入Agent 最大的价值是可以根据用户目标自主规划并调用工具。但并不是所有工具都适合完全自动执行。比如:删除数据库表删除文件发起转账修改线上配置调用外部系统执行不可逆操作这些动作一旦执行错误,影响可能非常大。所以比较合理的模式是:普通查询类动作可以让 Agent 自动执行,高风险动作必须先进入人工审批。在本文示例中,我们定义了三个工具:query_table_data:查询表数据,低风险,可以自动执行delete_table:删除数据表,高风险,需要人工审批delete_file:删除文件,高风险,需要人工审批用户输入的任务是:先查询product表的数据!再删除user表,最后,删除lucaju.txt文件Agent 会先分析任务,并尝试依次调用工具。但当执行到删除表、删除文件这类高风险动作时,会被框架中断,等待人工确认。二、初始化大模型首先初始化模型:import os from langchain.chat_models import init_chat_model llm = init_chat_model( model="kimi-k2.5", model_provider="openai", api_key=os.getenv("AliQwen_API"), base_url="https://dashscope.aliyuncs.com/compatible-mode/v1", model_kwargs={"reasoning_effort": "none"}, )这里使用的是 OpenAI 兼容模式,所以 model_provider 设置为 openai,同时把 base_url 指向阿里云 DashScope 的兼容接口。需要提前配置环境变量:export AliQwen_API="你的 API Key"model_kwargs={"reasoning_effort": "none"} 是模型调用参数,可以根据实际模型支持情况调整。三、定义工具示例中定义了三个工具:from langchain.tools import tool @tool def delete_table(table_name: str) -> str: """ 删除指定的表 """ return f"删除表{table_name}" @tool def delete_file(file_name: str) -> str: """ 删除指定的文件 """ return f"删除文件{file_name}" @tool def query_table_data(table_name: str) -> str: """ 查询指定表的数据 """ return f"查询表{table_name}的数据"这里为了演示效果,工具内部只是返回字符串,并没有真的连接数据库或删除文件。在真实项目中,delete_table 可能会执行 SQL,delete_file 可能会操作对象存储或服务器文件系统。这类工具就非常适合加人工审批。四、为什么必须配置 CheckpointerHuman-in-the-loop 的核心不是简单地打印一句“是否确认”,而是让 Agent 的执行流程真正暂停下来,并且后续可以从暂停点继续执行。这就需要保存执行状态。示例代码中使用了 LangGraph 提供的内存检查点:from langgraph.checkpoint.memory import InMemorySaver checkpointer = InMemorySaver() config = { "configurable": { "thread_id": "123" } }这里有两个关键点:checkpointer 用来保存 Agent 的执行状态。thread_id 用来标识当前会话。当 Agent 执行到需要人工介入的节点时,LangGraph 会把当前状态保存下来。等人工审批完成后,再通过同一个 thread_id 找回之前的状态,并继续执行。如果没有检查点,框架就不知道应该从哪里恢复执行。五、创建支持人工介入的 DeepAgent接下来创建 Agent:from deepagents import create_deep_agent main_agent = create_deep_agent( model=llm, name="主智能体", system_prompt="回答使用中文,调用对应的工具实现对应的功能!", tools=[delete_table, delete_file, query_table_data], interrupt_on={"delete_table": True, "delete_file": True}, checkpointer=checkpointer )这里最关键的是两个参数:interrupt_on={"delete_table": True, "delete_file": True}以及:checkpointer=checkpointerinterrupt_on 用来声明哪些工具调用需要中断。在这个例子中:调用 query_table_data 不会中断调用 delete_table 会中断调用 delete_file 会中断也就是说,Agent 可以自动查询数据,但不能自动删除表或文件。这就是人工介入的核心配置。六、第一次执行:触发中断第一次执行 Agent:result_1 = main_agent.invoke( { "messages": [ { "role": "user", "content": "先查询product表的数据!再删除user表,最后,删除lucaju.txt文件", } ] }, config=config, )这次调用并不一定会完整执行完所有工具。如果执行链路中包含需要人工审批的工具,Agent 会暂停,并把中断信息放到返回结果的 __interrupt__ 字段中。示例代码中这样获取:interrupt = result_1["__interrupt__"]当 interrupt 不为空时,说明当前执行过程中存在需要人工介入的动作。七、查看待审批动作__interrupt__ 中会包含本次待审批的工具调用信息,例如:[ Interrupt( value={ "action_requests": [ { "name": "delete_table", "args": {"table_name": "user"}, "description": "Tool execution requires approval..." }, { "name": "delete_file", "args": {"file_name": "zhaoweifeng.txt"}, "description": "Tool execution requires approval..." } ], "review_configs": [ { "action_name": "delete_table", "allowed_decisions": ["approve", "edit", "reject"] }, { "action_name": "delete_file", "allowed_decisions": ["approve", "edit", "reject"] } ] } ) ]这里有两个比较重要的字段。action_requests 表示 Agent 想要执行哪些工具:工具名称工具参数工具描述review_configs 表示这些动作允许哪些审批结果:approve:同意执行edit:修改参数后执行reject:拒绝执行这三个动作基本覆盖了常见的人审场景。八、人工审批:拒绝高危操作示例代码中把删除表和删除文件都拒绝掉:decisions = [] action_requests = interrupt[0].value["action_requests"] print(f"当前人机交互工具:{action_requests}") for action_request in action_requests: if action_request["name"] == "delete_table": decisions.append({"type": "reject"}) elif action_request["name"] == "delete_file": decisions.append({"type": "reject"})这里的 decisions 顺序需要和 action_requests 对应。也就是说,如果 Agent 申请了两个动作:删除 user 表删除 zhaoweifeng.txt 文件那么人工审批结果也应该按顺序给出两个 decision。在这个示例中,两个高风险动作都被拒绝:[ {"type": "reject"}, {"type": "reject"} ]九、第二次执行:恢复 Agent审批完成后,不需要重新传入用户消息,而是通过 Command(resume=...) 恢复执行:from langgraph.types import Command result_2 = main_agent.invoke( Command( resume={ "decisions": decisions } ), config=config, )这里一定要继续传入相同的 config,尤其是相同的 thread_id。因为 Agent 要根据 thread_id 找到之前暂停的执行状态。恢复执行后,框架会根据人工审批结果继续处理:被 approve 的工具会继续执行被 reject 的工具不会执行被 edit 的工具会使用人工修改后的参数执行最后输出结果:print(f"最终结果{result_2['messages'][-1].content}")十、完整代码完整示例代码如下:""" 演示human-in-the-loop模式, 必须使用记忆功能 """ import os from deepagents import create_deep_agent from langchain.chat_models import init_chat_model from langchain.tools import tool from langgraph.checkpoint.memory import InMemorySaver from langgraph.types import Command # 初始化大模型 llm = init_chat_model( model="kimi-k2.5", model_provider="openai", api_key=os.getenv("AliQwen_API"), base_url="https://dashscope.aliyuncs.com/compatible-mode/v1", model_kwargs={"reasoning_effort": "none"}, ) # 删除表工具 @tool def delete_table(table_name: str) -> str: """ 删除指定的表 """ return f"删除表{table_name}" # 删除文件 @tool def delete_file(file_name: str) -> str: """ 删除指定的文件 """ return f"删除文件{file_name}" # 查询表数据 @tool def query_table_data(table_name: str) -> str: """ 查询指定表的数据 """ return f"查询表{table_name}的数据" # 设置检查点 checkpointer = InMemorySaver() # 配置检查点 config = { "configurable": { "thread_id": "123" } } # 创建deepagent,同时给高危工具设置人机交互 main_agent = create_deep_agent( model=llm, name="主智能体", system_prompt="回答使用中文,调用对应的工具实现对应的功能!", tools=[delete_table, delete_file, query_table_data], interrupt_on={"delete_table": True, "delete_file": True}, checkpointer=checkpointer ) # 预执行,本次不会真正的执行所有工具。 # 如果执行链路中存在人机交互节点,框架会暂停,并返回 __interrupt__。 result_1 = main_agent.invoke( { "messages": [ { "role": "user", "content": "先查询product表的数据!再删除user表,最后,删除lucaju.txt文件", } ] }, config=config, ) # 检查本次执行是否存在人机交互动作 interrupt = result_1["__interrupt__"] if interrupt: print("存在人机交互动作") # 定义一个列表,存储所有审批结果 decisions = [] # 获取所有待审批动作 action_requests = interrupt[0].value["action_requests"] print(f"当前人机交互工具:{action_requests}") for action_request in action_requests: if action_request["name"] == "delete_table": decisions.append({"type": "reject"}) elif action_request["name"] == "delete_file": decisions.append({"type": "reject"}) # 再次执行,不需要传会话内容,只需要传审批意见和 config result_2 = main_agent.invoke( Command( resume={ "decisions": decisions } ), config=config, ) print(f"最终结果{result_2['messages'][-1].content}")十一、如果想修改参数后再执行除了直接拒绝,我们也可以修改工具参数后再执行。比如 Agent 原本想删除:zhaoweifeng.txt人工审批时可以把文件名改成另一个值:decisions.append({ "type": "edit", "edited_action": { "name": action_request["name"], "args": { "file_name": "new-file.txt" } } })这时框架不会使用 Agent 原始生成的参数,而是使用人工编辑后的参数继续执行工具。这在实际业务中非常有用。例如:Agent 选择的表名不准确,人工改成正确表名Agent 生成的文件路径不安全,人工改成允许路径Agent 生成的金额过大,人工改成合理金额Agent 生成的收件人错误,人工改成正确收件人相比简单的同意或拒绝,edit 让人工介入更灵活。十二、执行流程总结整体流程可以概括为:用户输入任务 ↓ Agent 分析任务并规划工具调用 ↓ 普通工具自动执行 ↓ 遇到 interrupt_on 配置的高风险工具 ↓ LangGraph 保存状态并中断执行 ↓ 人工读取 __interrupt__ 中的 action_requests ↓ 人工给出 approve / edit / reject ↓ 使用 Command(resume=...) 恢复执行 ↓ Agent 根据审批结果继续完成任务这套机制的关键点是:interrupt_on:定义哪些工具需要人工审批checkpointer:保存执行状态thread_id:标识同一次会话__interrupt__:获取待审批动作Command(resume=...):把审批结果送回 Agent 并恢复执行十三、和 Spring AI Alibaba 版本的对比从思想上看,Python 版本和 Spring AI Alibaba 版本是一致的:Agent 不应该无限制地自动执行所有动作,高风险动作需要进入人工审批流程。但实现方式上有所不同。Spring AI Alibaba 更偏向 Java 生态,适合和 Spring Boot、企业系统、审批流、权限体系结合。而 LangChain + LangGraph + DeepAgents 的 Python 方案更偏向实验、原型验证和 Agent 工作流编排。尤其是 LangGraph 的 checkpoint 和 resume 机制,让“暂停后恢复”这件事变得非常自然。如果项目本身是 Java 技术栈,可以优先考虑 Spring AI Alibaba。如果项目本身是 Python 技术栈,或者正在做 Agent 编排、工具调用、多步骤任务规划,那么 DeepAgents 这套方式会比较顺手。十四、真实项目中的建议在真实项目中使用人工介入时,建议注意以下几点。第一,高风险工具要显式配置。不要只依赖 prompt 告诉模型“删除前要确认”。更可靠的方式是像本文一样,在框架层面对工具进行拦截。第二,审批信息要完整展示。人工审批时至少要看到:工具名称工具参数Agent 为什么要执行这个工具当前用户是谁当前会话上下文第三,审批结果要落库。生产环境中,审批记录应该持久化,包括:谁审批的什么时间审批的原始参数是什么修改后的参数是什么最终是通过、拒绝还是编辑后通过第四,检查点不要只用内存。本文为了演示使用的是:InMemorySaver()生产环境更建议使用数据库、Redis 或其他持久化存储。否则服务重启后,暂停中的 Agent 状态会丢失。第五,审批权限要和业务系统打通。不是所有人都应该可以批准所有工具调用。比如:普通用户只能审批自己的任务管理员可以审批团队任务涉及资金、删除、发布的动作需要更高权限人工介入不是简单的弹窗确认,而应该是完整的安全控制链路。关于DeepAgents的练习代码已经上传到了我的GitHub,欢迎fork & starhttps://github.com/Jucunqi/deepagents-practice.git
2026年04月30日
56 阅读
0 评论
1 点赞
1
2
...
12