[{"data":1,"prerenderedAt":397},["ShallowReactive",2],{"content:article:langchain4j-agent-boundaries":3,"content:article-surround:\u002Farticles\u002Flangchain4j-agent-boundaries":392},{"id":4,"title":5,"body":6,"category":376,"cover":377,"date":378,"description":379,"draft":380,"extension":381,"featured":380,"meta":382,"navigation":77,"path":383,"seo":384,"stem":385,"tags":386,"updated":390,"__hash__":391},"articles\u002Farticles\u002Flangchain4j-agent-boundaries.md","用了 LangChain4j 之后，我为什么还是感觉在\"手搓\"Agent",{"type":7,"value":8,"toc":367},"minimark",[9,16,20,23,31,85,100,103,106,109,152,159,163,176,193,196,260,286,293,297,307,311,323,327,330,333,336,344,350,357,360,363],[10,11,12],"blockquote",{},[13,14,15],"p",{},"一个 Java 开发者从零构建两个 LLM Agent 应用的实战记录。项目背景：Gym-Agent（体育场馆预约平台的 AI 助手，Spring Boot 3 + LangChain4j + pgvector RAG）和 Job-Agent（求职全链路 Agent，Spring Boot 3 + LangChain4j + Planner-Executor 架构）。所有踩坑均来自真实代码，细节略有脱敏。",[17,18,19],"h2",{"id":19},"事情的起点",[13,21,22],{},"去年年中我给自己挖了个坑：给一个跑了两年的体育场馆预约系统（Spring Boot 3 + MyBatis + Vue3 的老组合）加一个 AI 助手，后来这个项目叫 Gym-Agent。需求听起来很朴素——用户说\"帮我看看明天下午有没有羽毛球场\"，系统能听懂、能查、能约。",[13,24,25,26,30],{},"AI 这块的技术选型没纠结太久。Java 圈里能打的 Agent 框架就那么几个，对比了一圈之后选了 LangChain4j 1.13.1。打动我的是文档里的一个例子：用 ",[27,28,29],"code",{},"AiServices"," 把一个接口、一个模型、几个工具拼起来，Agent 就活了。",[32,33,38],"pre",{"className":34,"code":35,"language":36,"meta":37,"style":37},"language-java shiki shiki-themes github-light github-dark","GymAgentAssistant assistant = AiServices.builder(GymAgentAssistant.class)\n        .chatModel(chatModel)                                  \u002F\u002F OpenAI 兼容接口\n        .chatMemoryProvider(memId -> new RedisChatMemory(memId))\n        .tools(queryTools, bookingTools, ragTools)\n        .build();\n\nString answer = assistant.chat(\"明天下午有羽毛球场吗？\", userId);\n","java","",[27,39,40,48,54,60,66,72,79],{"__ignoreMap":37},[41,42,45],"span",{"class":43,"line":44},"line",1,[41,46,47],{},"GymAgentAssistant assistant = AiServices.builder(GymAgentAssistant.class)\n",[41,49,51],{"class":43,"line":50},2,[41,52,53],{},"        .chatModel(chatModel)                                  \u002F\u002F OpenAI 兼容接口\n",[41,55,57],{"class":43,"line":56},3,[41,58,59],{},"        .chatMemoryProvider(memId -> new RedisChatMemory(memId))\n",[41,61,63],{"class":43,"line":62},4,[41,64,65],{},"        .tools(queryTools, bookingTools, ragTools)\n",[41,67,69],{"class":43,"line":68},5,[41,70,71],{},"        .build();\n",[41,73,75],{"class":43,"line":74},6,[41,76,78],{"emptyLinePlaceholder":77},true,"\n",[41,80,82],{"class":43,"line":81},7,[41,83,84],{},"String answer = assistant.chat(\"明天下午有羽毛球场吗？\", userId);\n",[13,86,87,88,91,92,95,96,99],{},"真的就这么几行。接的是 OpenAI 兼容 API，",[27,89,90],{},"baseUrl"," + ",[27,93,94],{},"apiKey"," 配上就能指向任意云端模型。Demo 跑通那天晚上，我在工位上反复问它\"明天能约乒乓球吗\"\"帮我订周五的场\"，看着日志里模型规规矩矩地调用 ",[27,97,98],{},"queryVenues","、生成预约草稿，心里想的是：Agent 开发的门槛，已经被框架抹平了。",[13,101,102],{},"三个月后回头看，这个想法错得离谱。",[17,104,105],{"id":105},"框架真正替你解决的问题",[13,107,108],{},"先说公道话，LangChain4j 干的活是有价值的，而且是那种不体面的脏活：",[110,111,112,123,137,146],"ul",{},[113,114,115,119,120,122],"li",{},[116,117,118],"strong",{},"模型协议封装","。OpenAI 兼容格式的请求拼装、流式响应解析、错误重试，接火山引擎、通义千问都是换个 ",[27,121,90],{}," 的事。",[113,124,125,128,129,132,133,136],{},[116,126,127],{},"Function Calling 的序列化","。",[27,130,131],{},"@Tool"," 注解标在方法上，参数用 ",[27,134,135],{},"@P"," 写描述，框架自动生成 JSON Schema 发给模型；模型返回 tool call 之后，框架反射调用方法、把结果再喂回模型。这一进一出自己手写 HTTP 的话，光是参数校验和异常兜底就够喝一壶。",[113,138,139,128,142,145],{},[116,140,141],{},"对话记忆容器",[27,143,144],{},"ChatMemoryProvider"," 按 memoryId 隔离对话，窗口截断也是现成的。",[113,147,148,151],{},[116,149,150],{},"EmbeddingStore 抽象","。pgvector 实现即插即用，RAG 的最小可用版本二十行代码。",[13,153,154,155,158],{},"这些如果都自己写，项目会从\"做 Agent\"变成\"做 SDK\"。所以问题从来不是\"框架有没有用\"，而是——",[116,156,157],{},"它的边界在哪里","。下面是我撞过的四面墙。",[17,160,162],{"id":161},"第一面墙tool-没有元数据","第一面墙：@Tool 没有\"元数据\"",[13,164,165,167,168,171,172,175],{},[27,166,131],{}," 注解能携带的信息只有 name 和 description，而这两个字段是",[116,169,170],{},"给模型看的","。但我的系统需要另一套信息，是",[116,173,174],{},"给系统自己看的","：",[110,177,178,181,184,187,190],{},[113,179,180],{},"这个工具需要登录态吗？",[113,182,183],{},"风险等级是什么？只读、写入、还是不可逆？",[113,185,186],{},"需要用户二次确认吗？",[113,188,189],{},"要不要限流？",[113,191,192],{},"参数的校验规则是什么？",[13,194,195],{},"这些信息模型不需要知道，但权限系统、审计日志、安全策略全都依赖它。框架没有这个概念，那就自己造：",[32,197,199],{"className":34,"code":198,"language":36,"meta":37,"style":37},"public interface GymAgentToolExecutor {\n    String toolCode();\n    String toolName();\n    String description();        \u002F\u002F 给模型看的\n    RiskLevel riskLevel();       \u002F\u002F READ \u002F WRITE \u002F IRREVERSIBLE，给系统看的\n    boolean needLogin();\n    boolean needConfirm();\n    int rateLimitSeconds();\n    List\u003CToolParamDefinition> paramDefinitions();\n    ToolResult execute(ToolContext ctx, Map\u003CString, Object> params);\n}\n",[27,200,201,206,211,216,221,226,231,236,242,248,254],{"__ignoreMap":37},[41,202,203],{"class":43,"line":44},[41,204,205],{},"public interface GymAgentToolExecutor {\n",[41,207,208],{"class":43,"line":50},[41,209,210],{},"    String toolCode();\n",[41,212,213],{"class":43,"line":56},[41,214,215],{},"    String toolName();\n",[41,217,218],{"class":43,"line":62},[41,219,220],{},"    String description();        \u002F\u002F 给模型看的\n",[41,222,223],{"class":43,"line":68},[41,224,225],{},"    RiskLevel riskLevel();       \u002F\u002F READ \u002F WRITE \u002F IRREVERSIBLE，给系统看的\n",[41,227,228],{"class":43,"line":74},[41,229,230],{},"    boolean needLogin();\n",[41,232,233],{"class":43,"line":81},[41,234,235],{},"    boolean needConfirm();\n",[41,237,239],{"class":43,"line":238},8,[41,240,241],{},"    int rateLimitSeconds();\n",[41,243,245],{"class":43,"line":244},9,[41,246,247],{},"    List\u003CToolParamDefinition> paramDefinitions();\n",[41,249,251],{"class":43,"line":250},10,[41,252,253],{},"    ToolResult execute(ToolContext ctx, Map\u003CString, Object> params);\n",[41,255,257],{"class":43,"line":256},11,[41,258,259],{},"}\n",[13,261,262,263,266,267,270,271,273,274,277,278,281,282,285],{},"原来的工具类（",[27,264,265],{},"GymQueryTools","、",[27,268,269],{},"GymBookingTools"," 这些）退化成一层薄壳，",[27,272,131],{}," 方法只负责接参数，转手丢给对应的 executor。再写一个 ",[27,275,276],{},"GymAgentToolRegistry","，",[27,279,280],{},"@PostConstruct"," 里扫描所有 ",[27,283,284],{},"GymAgentToolExecutor"," 实现类自动注册——写到这里我突然意识到，我这是照着 Spring 的套路，在框架之上又发明了一遍 IoC。",[13,287,288,289,292],{},"这部分代码不复杂，但它决定了后面所有安全设计（草稿确认、权限拦截、限流）能不能落地。",[116,290,291],{},"工具的元数据是 Agent 治理的地基","，而框架对此一无所知。",[17,294,296],{"id":295},"第二面墙记忆不是-chatmemory","第二面墙：记忆不是 ChatMemory",[13,298,299,302,303,306],{},[27,300,301],{},"MessageWindowChatMemory"," 给你一个滑动窗口，仅此而已。用户上一句说\"羽毛球馆\"，下一句说\"那个场地的价格呢\"，模型能接住，因为还在窗口里。但窗口外的世界一片漆黑：跨会话、长周期偏好、记忆冲突，全都得自己搓。这一块后来在 Job-Agent 里长成了一整个 ",[27,304,305],{},"memory\u002F"," 包，踩的坑足够单独写一篇（见本系列第三篇）。",[17,308,310],{"id":309},"第三面墙可观测性是个黑盒","第三面墙：可观测性是个黑盒",[13,312,313,315,316,91,319,322],{},[27,314,29],{}," 返回的是动态代理，请求进去、答案出来，中间模型调了哪个工具、参数是什么、耗时多少，框架外面看不见。等你想加日志的时候，会发现一脚踏进 Spring AOP 和框架反射机制打架的泥潭——这个坑我 debug 到凌晨一点，值得单开一篇（见本系列第五篇）。最后的结果是搓了一套 ",[27,317,318],{},"AgentToolLogHelper",[27,320,321],{},"AgentTraceContext","，用 traceId 把一次提问触发的多次工具调用串成一棵树。",[17,324,326],{"id":325},"第四面墙框架没有执行前的钩子","第四面墙：框架没有\"执行前\"的钩子",[13,328,329],{},"Agent 时代最要命的安全问题是：模型决定调用一个有副作用的工具时，系统没有官方途径说\"等一下，先问问用户\"。LangChain4j 的工具执行是直线型的：模型发出 tool call，框架反射执行，结果返回。想在中间插一道人机确认关卡，只能靠自己的双层工具设计——草稿与确认分离，这整套东西（包括后来 Job-Agent 里升级成的\"行动确认中心\"）都是手搓的，见第二篇。",[17,331,332],{"id":332},"第二个项目给出的最终答案",[13,334,335],{},"后来做 Job-Agent（求职全链路 Agent，覆盖简历准备、岗位匹配、投递跟进、模拟面试），LangChain4j 升到 1.15.1 并换用官方 Spring Boot Starter，我学乖了，第一天就把架构分层定死：",[32,337,342],{"className":338,"code":340,"language":341},[339],"language-text","Job-Agent-Bootstrap\u002F       \u002F\u002F 业务编排层\n  └── agent\u002F\n      ├── intent\u002F    \u002F\u002F Planner：意图拆解\n      ├── plan\u002F      \u002F\u002F 计划管理、状态机\n      ├── executor\u002F  \u002F\u002F AgentToolInvoker + 三层执行结果\n      ├── guardrail\u002F \u002F\u002F 注入防护、越权拦截、PII 脱敏\n      ├── memory\u002F    \u002F\u002F 记忆提取、冲突检测、版本历史\n      └── schema\u002F    \u002F\u002F 工具四层 Schema\nJob-Agent-Infra-Ai\u002F        \u002F\u002F LangChain4j 被压在这一层\n","text",[27,343,340],{"__ignoreMap":37},[13,345,346,347,128],{},"LangChain4j 被压在 Infra-Ai 一层，只干模型交互这一件事。项目跑了一年，回头估一下代码量：和 LangChain4j 直接相关的配置与调用，占比不到 5%。剩下 95% 是 Planner、执行器、护栏、记忆、可观测、Eval——",[116,348,349],{},"Agent 的骨架，全是自己搓的",[13,351,352,353,356],{},"所以现在再看到\"要不要手搓 Agent\"的争论，我的看法是：这个问题本身是个伪命题。",[116,354,355],{},"不管你用不用框架，你都在手搓 Agent","——框架只是替你搓了最不重要的那一小块（模型交互层），而真正的难点（治理、安全、记忆、评估）它一行都不会替你写。",[13,358,359],{},"选型时真正该问的不是\"哪个框架功能多\"，而是两个更实际的问题：**它的抽象和你的业务贴不贴？越界的时候好不好绕过去？**这一年用下来，LangChain4j 的抽象边界大体是清晰的，出问题的时候（比如上面那面 AOP 的墙）也有绕的余地。丑话说在前面：1.x 早期版本 API 变动不小，从 1.13 升到 1.15 我跟了两次升级，每次都要小改一圈。框架是拿来省力的，不是拿来信仰的。",[13,361,362],{},"下一篇讲这个系列里最惊心动魄的一件事：Agent 未经确认，直接帮用户把场地订了。",[364,365,366],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":37,"searchDepth":56,"depth":56,"links":368},[369,370,371,372,373,374,375],{"id":19,"depth":50,"text":19},{"id":105,"depth":50,"text":105},{"id":161,"depth":50,"text":162},{"id":295,"depth":50,"text":296},{"id":309,"depth":50,"text":310},{"id":325,"depth":50,"text":326},{"id":332,"depth":50,"text":332},"LLM 应用","\u002Fimages\u002Fcovers\u002Flangchain4j-agent-boundaries.jpg","2025-09-14","框架的能力边界与四个撞墙瞬间",false,"md",{},"\u002Farticles\u002Flangchain4j-agent-boundaries",{"title":5,"description":379},"articles\u002Flangchain4j-agent-boundaries",[387,388,389],"Agent","LangChain4j","架构设计",null,"TsAOG9od9RUNflcl8XQSTEdOXxN1LxoibDLVTFelpNs",[393,390],{"title":394,"path":395,"date":396},"别让 Agent 直接动手：我用\"草稿-确认\"双阶段管住高风险操作","\u002Farticles\u002Fagent-draft-confirm-safety","2025-10-08",1792063306799]