资讯动态

Java Agent工程化:从Agent Loop到Graph Engineering的实践

发布时间:2026/10/14 21:33:58 来源:尧图企业网站定制
如果只用一个while (true)循环去写 Java Agent第一周你会觉得“AI 不过如此”第二个月你会觉得“这 Agent 根本收不住”。业务方不断加条件先查指标、再查规则、规则查不到就走兜底话术、兜底还要带上上下文……代码里的 if/else 和状态变量越来越多模型调用散落在各个分支里日志看起来像一堆随机打印。这不是你写代码的能力问题而是 Agent 开发范式本身需要升级从 Agent Loop 到 Graph Engineering。这篇文章不打算空谈“Graph 比 Loop 高级”。我会从一个真实可落地的场景切入业务分析 → 知识库检索 → Agent 决策逐步讲清楚为什么图式编排更适合 Java 工程化项目并给出一套可以直接跑的 Java 样例代码。读完你至少能回答三个问题Java 里怎么定义 Agent 节点怎么用“边”代替一坨 if/else知识库检索结果如何参与 Agent 决策而不是被模型随口编造1. 为什么 Java 工程化 Agent 需要从 Loop 走向 Graph Engineering1.1 先看一个真实的“Agent Loop”困境很多 Java 团队刚开始做 Agent 时走的都是同一条路把大模型 API 封装成一个chat()方法然后在一个循环里反复调用直到模型说“我已完成”。// 伪代码一个典型的 Agent Loop while (true) { String answer chatModel.call(prompt history); history.add(answer); if (answer.contains(FINISH)) { break; } // 把所有分支逻辑都揉在循环里 if (answer.contains(SEARCH_KB)) { // 检索知识库 } else if (answer.contains(CALL_API)) { // 调用业务接口 } }在小范围 Demo 里这种写法完全成立。它直观、易上手模型本身也具备一定的推理能力你甚至可以让模型自己决定下一步做什么。但随着业务复杂化问题会集中爆发状态不可见整个 Agent 的中间状态都被塞进一个循环变量或者越来越长的 history 里出问题时很难定位是哪一步导致模型判断偏差。分支不可控模型说“检索知识库”就检索知识库说“调业务接口”就调接口。上线后你根本没法保证它不把这两个动作搞反更没法做灰度。复用性差一旦第二个业务场景需要“先查订单再分析”你只能复制一个循环改里面的判断逻辑。第三个、第四个场景逐渐变成样板代码灾难。换句话说Loop 是“把控制权交给模型”Graph Engineering 是“把控制权掌握在工程手里模型只负责节点内部的任务”。1.2 Graph Engineering 到底改了什么Graph Engineering 的核心抽象不再是“循环 分支”而是“节点 边 状态”。对比维度Agent LoopGraph Engineering控制结构循环和 if/else有向图节点、边、条件路由状态管理散落在局部变量和历史消息统一的上下文对象在节点间传递分支决策模型自由发挥节点输出结构化结果由路由决定下一步可观测性看日志猜状态每个节点有明确输入输出可打点可追踪可复用性复制项目再改节点独立可组合不同业务图可回滚整个循环从头再来支持指定节点回退和补偿从工程视角看Graph Engineering 更像是一个“可编排的任务流水线”。它不排斥模型智能而是把模型的自由裁量约束在节点内部把流程稳定交给图来保障。对于 Java 这种强调工程化、团队协作、代码可维护性的语言来说这个范式贴近后端工程的心智。1.3 什么场景必须用 Graph不是所有 Agent 都需要 Graph。单轮问答、简单文本总结、纯粹聊天场景用 Loop 甚至都不用 Loop直接一次调用即可。但下面几类场景强烈建议 Graph 化多阶段业务分析先理解用户诉求再拆指标再查规则最后给结论。知识库问答需要先检索再把检索片段拼入提示词模型只能基于片段作答。多工具编排查订单、查库存、计算价格、生成回复工具之间存在先后依赖。有人工审核环节Agent 产出中间结果后需要暂停等待确认再继续后续节点。需要观测和审计每一步都希望留下结构化记录而不是一段上下文。本文后面要实现的业务分析 → 知识库 → Agent 决策场景恰好覆盖了前三类。2. 理解 Agent 开发的两代范式Agent Loop 与 Graph Engineering2.1 Agent Loop 的本质Agent Loop 的理论原型是 ReAct 模式模型在“推理 - 行动 - 观察”之间循环直到完成目标。每一步模型输出一个行动指令程序执行行动把结果作为新的观察喂回模型。它的优点在于实现简单适合在非生产环境做概念验证缺点在于流程弹性完全依赖模型的自我约束。模型一旦在循环中给出错误指令程序只能按照错误指令执行异常难以在结束时回溯。工程上看这个模式缺少“边界”和“兜底”。2.2 Graph Engineering 的核心抽象节点、边、状态Graph Engineering 把任务建模为一个有向图图中每个元素都有明确职责节点Node一个独立的处理单元。它接收状态输出状态。节点内部可以是模型调用、知识库检索、工具调用也可以是一个普通的规则函数。边Edge节点之间的连接关系描述“上一步完成后下一步去哪”。普通边是必然路径条件边由路由函数根据状态决定。状态State在节点间流转的上下文对象。通常是一个结构化数据例如MapString, Object或者自定义 DTO。状态是 Graph 的“血液”。起始节点与终止节点图需要有一个明确的入口以及一个或多个终止条件。终止节点不代表任务“成功”只代表流程走到终点。类比来说Loop 像是一个员工自己决定今天干什么出了问题只能追着员工问Graph 像是一条装配线每个工位只完成自己的工序由传送带状态把半成品送到下一站质检员条件路由决定返工还是放行。2.3 什么时候 Loop 够用什么时候必须 Graph判断标准不取决于项目大小而取决于“模型输出的不确定路径会不会产生不可接受的副作用”。Loop 够用的场景模型只生成文字不调用外部动作或者即使调用工具出错后果也可接受或者整个流程只有两三个步骤人工可以在旁边盯守。必须 Graph 或至少结构化编排的场景模型输出的中间结果会影响后续系统动作例如写订单、发消息、查询敏感数据多个外部系统之间的调用顺序不能乱业务流程需要审计和回滚。如果项目已经出现以下信号说明是时候放弃纯 Loop 了一个方法里的if/else超过 5 个且每个分支里都有模型调用。调试时需要反复回看十几条日志才能确认模型刚才做了什么。产品经理开始提“这个场景和那个场景流程不一样”的需求。团队里有人开始复制上一个 Agent 的代码只改提示词。3. Java 生态下的落地选型与架构设计3.1 Java Agent 框架怎么选Java 生态里做 Agent目前主流选择大致三类方案特点适用情况LangChain4jJava 生态的 LLM 框架提供模型接入、RAG、AI Services想快速接入大模型同时希望保留 Java 工程习惯Spring AISpring 官方生态的 AI 框架与 Spring Boot 集成自然团队已经是 Spring Boot 技术栈希望统一依赖管理自研 Graph 运行时自己定义节点和边完全掌控流程业务规则敏感需要定制化编排不想绑框架本文的核心目标是讲清思路所以示例采用“自研最小 Graph 运行时 LangChain4j 接入模型能力”的方式。自研 Graph 运行时代码量不大却能让你彻底理解节点、边、状态的关系。生产环境可以在此基础上扩展成团队内部的 Agent 编排平台。3.2 三层架构设计业务分析 → 知识库 → Agent 决策以一个具体的业务需求为例销售运营同学提问“本月华东区销售额为什么比上月下降应该重点跟进哪些客户”传统做法是人工查数、看规则、写结论我们希望用一个 Agent 来自动化第一步的分析框架和第三步的建议生成。整个 Agent 流程拆成三个节点业务分析节点BusinessAnalyzerNode把用户问题解析成结构化分析任务输出分析对象、时间范围、对比口径。知识库检索节点KnowledgeBaseRetrieverNode根据结构化分析任务从企业内部知识库检索指标定义、业务规则、历史分析案例。Agent 决策节点AgentDecisionNode结合业务分析结果和检索到的知识片段生成最终回答或行动建议。三者的关系是业务分析节点解决“用户到底要分析什么”知识库检索节点解决“分析依据是什么”Agent 决策节点解决“结论和建议是什么”。每个节点内部可以调用大模型但节点之间的流转由 Graph 运行时控制。3.3 状态对象设计节点之间传递的上下文对象我建议用一个可序列化的MapString, Object或自定义 DTO。Map 灵活但字段不明确自定义 DTO 严格但扩展性差。工程折中方案是定义一个AgentContext对象内部持有一个MapString, Object同时提供类型安全的读取方法。// 文件路径src/main/java/com/example/agent/graph/AgentContext.java public class AgentContext { private final MapString, Object data new HashMap(); public void put(String key, Object value) { data.put(key, value); } SuppressWarnings(unchecked) public T T get(String key, ClassT type) { Object value data.get(key); if (value null) { return null; } if (type.isInstance(value)) { return (T) value; } throw new IllegalStateException(状态字段类型不匹配: key); } public MapString, Object snapshot() { return new HashMap(data); } }统一状态对象的价值在于每个节点只需要知道自己要读什么字段、写什么字段不需要和其他节点共享局部变量。这大幅提升了节点的可测试性也为后续做日志埋点提供了统一入口。4. 环境准备与工程骨架4.1 运行环境本文示例代码基于 Java 17 和 Maven 工程理论上 Java 11 也可以运行但推荐 Java 17 以上因为代码中会用到部分现代语法。大模型接入部分使用 OpenAI 兼容协议的 APIbase-url和api-key请按照你的模型服务商实际情况配置。不需要 Spring Boot 也能跑通但如果你项目的技术栈是 Spring Boot可以把 Graph 节点交给 Spring 容器管理实现节点的自动注入和配置化组装。4.2 Maven 依赖代码中会用到 LangChain4j 的模型接口和文本切分能力。这里只列出关键依赖具体版本请以官方最新稳定版为准注意不同版本之间的 API 差异。!-- 文件路径pom.xml -- dependencies !-- LangChain4j 核心 -- dependency groupIddev.langchain4j/groupId artifactIdlangchain4j/artifactId version${langchain4j.version}/version /dependency !-- OpenAI 兼容协议接入 -- dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-open-ai/artifactId version${langchain4j.version}/version /dependency !-- 本地向量嵌入模型示例用生产可替换 -- dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-embeddings-all-minilm-l6-v2/artifactId version${langchain4j.version}/version /dependency !-- 日志 -- dependency groupIdorg.slf4j/groupId artifactIdslf4j-api/artifactId version2.0.13/version /dependency dependency groupIdch.qos.logback/groupId artifactIdlogback-classic/artifactId version1.5.6/version /dependency /dependencies4.3 模型配置不建议把 API Key 写死在代码里。通过环境变量注入并在代码启动时读取。# 文件路径src/main/resources/application.properties如果使用 Spring Boot llm.base-url${LLM_BASE_URL} llm.api-key${LLM_API_KEY} llm.model-name${LLM_MODEL_NAME}不使用 Spring Boot 时也可以在启动类的main方法里通过System.getenv()读取。4.4 工程目录结构为了直观推荐按下面的结构组织代码src/main/java/com/example/agent/ ├── graph/ # Graph 运行时核心 │ ├── Node.java │ ├── AgentGraph.java │ └── AgentContext.java ├── node/ # 业务节点 │ ├── BusinessAnalyzerNode.java │ ├── KnowledgeBaseRetrieverNode.java │ └── AgentDecisionNode.java ├── llm/ # 大模型配置 │ └── LlmFactory.java └── AgentApplication.java # 启动入口5. 核心代码实现定义 Graph 运行时5.1 Node 接口节点是图里的基本执行单元。每个节点有一个名字、一个执行方法执行方法接收上下文处理后写回上下文。// 文件路径src/main/java/com/example/agent/graph/Node.java public interface Node { String name(); void execute(AgentContext context); }这里把返回值设计为void而不是Map是因为状态统一通过AgentContext传递节点没有独立返回值所有产物都写入上下文。这样图运行时只需要维护一个上下文实例天然避免“返回新对象后其他节点拿不到”的问题。5.2 AgentGraph 运行时运行时需要支持注册节点。配置每个节点执行完之后下一个节点是谁。支持条件边根据上下文中的某个字段动态决定下一节点。设置最大执行步数防止图出现死循环。// 文件路径src/main/java/com/example/agent/graph/AgentGraph.java public class AgentGraph { private final MapString, Node nodes new LinkedHashMap(); private final MapString, ListString edges new HashMap(); private final MapString, ListConditionalEdge conditionalEdges new HashMap(); private String entryNode; private int maxSteps 20; public AgentGraph addNode(Node node) { nodes.put(node.name(), node); return this; } public AgentGraph edge(String from, String to) { edges.computeIfAbsent(from, k - new ArrayList()).add(to); return this; } public AgentGraph conditionalEdge(String from, ConditionalEdge edge) { conditionalEdges.computeIfAbsent(from, k - new ArrayList()).add(edge); return this; } public AgentGraph entry(String nodeName) { this.entryNode nodeName; return this; } public AgentGraph maxSteps(int maxSteps) { this.maxSteps maxSteps; return this; } public AgentContext run(AgentContext inputContext) { if (entryNode null || !nodes.containsKey(entryNode)) { throw new IllegalStateException(入口节点未配置或不存在); } String currentNode entryNode; AgentContext context inputContext; int step 0; while (currentNode ! null step maxSteps) { Node node nodes.get(currentNode); System.out.println([Graph] 执行节点: node.name() (step step )); node.execute(context); String next decideNext(currentNode, context); if (next null) { break; } currentNode next; step; } if (step maxSteps) { throw new IllegalStateException(Agent 执行超过最大步数疑似出现死循环); } return context; } private String decideNext(String currentNode, AgentContext context) { ListConditionalEdge conditionals conditionalEdges.get(currentNode); if (conditionals ! null) { for (ConditionalEdge ce : conditionals) { if (ce.evaluate(context)) { System.out.println([Graph] 条件边命中: currentNode - ce.targetNode()); return ce.targetNode(); } } } ListString nextNodes edges.get(currentNode); if (nextNodes ! null !nextNodes.isEmpty()) { return nextNodes.get(0); } return null; } }5.3 条件边的定义条件边本质是一个“谓词 目标节点”的组合。// 文件路径src/main/java/com/example/agent/graph/ConditionalEdge.java public class ConditionalEdge { private final String targetNode; private final PredicateAgentContext condition; public ConditionalEdge(String targetNode, PredicateAgentContext condition) { this.targetNode targetNode; this.condition condition; } public boolean evaluate(AgentContext context) { return condition.test(context); } public String targetNode() { return targetNode; } }有这个抽象之后原本散落在 Loop 里的 if/else 就变成了图上的一条条条件边。比如“如果分析结果里缺少时间范围就回到业务分析节点重新解析”这种需求只需要在图上配置一条条件边即可业务逻辑清晰很多。5.4 为什么自定义 Graph 比直接用框架更值得写可能有人会问LangChain4j 和 Spring AI 不是已经提供了 Agent 能力吗为什么要自己写一个 100 行的图运行时原因是图运行时是 Agent 工程化的“管道”这部分自己做能确保完全可控模型调用、向量检索是“水”这部分可以用框架。通信基础设施自己掌控业务连接用成熟组件这是 Java 工程里常见的组合方式。等团队跑通几个图之后如果觉得需要更成熟的图能力再把运行时替换成 LangGraph4j 或自研平台也不迟。6. 业务节点实战业务分析 → 知识库检索 → Agent 决策6.1 大模型工厂配置定义一个简单的工厂用来创建ChatLanguageModel。这段代码使用了 LangChain4j 的 OpenAI 兼容配置。// 文件路径src/main/java/com/example/agent/llm/LlmFactory.java package com.example.agent.llm; import dev.langchain4j.model.chat.ChatLanguageModel; import dev.langchain4j.model.openai.OpenAiChatModel; import java.time.Duration; public class LlmFactory { public static ChatLanguageModel create() { String baseUrl System.getenv(LLM_BASE_URL); String apiKey System.getenv(LLM_API_KEY); String modelName System.getenv(LLM_MODEL_NAME); return OpenAiChatModel.builder() .baseUrl(baseUrl) .apiKey(apiKey) .modelName(modelName) .timeout(Duration.ofSeconds(60)) .temperature(0.2) .build(); } }temperature设置得比较低目的是让业务分析节点的输出更稳定。工程化 Agent 中业务环节的高确定性比创造性更重要。6.2 业务分析节点拆解用户问题节点输入用户原始问题例如“本月华东区销售额为什么比上月下降应该重点跟进哪些客户”节点输出一个结构化的分析计划包含分析对象、时间范围、对比口径、关注指标。// 文件路径src/main/java/com/example/agent/node/BusinessAnalyzerNode.java package com.example.agent.node; import com.example.agent.graph.AgentContext; import com.example.agent.graph.Node; import dev.langchain4j.model.chat.ChatLanguageModel; public class BusinessAnalyzerNode implements Node { private final ChatLanguageModel model; public BusinessAnalyzerNode(ChatLanguageModel model) { this.model model; } Override public String name() { return 业务分析; } Override public void execute(AgentContext context) { String userQuestion context.get(userQuestion, String.class); String prompt 你是一个业务分析规划器。请把用户的业务问题拆解为结构化分析计划只输出 JSON。 用户问题 %s 输出格式 { analysisObject: 分析对象例如华东区销售, timeRange: 时间范围例如本月、上月, compareType: 对比口径例如环比下降, metrics: [指标1, 指标2] } .formatted(userQuestion); String rawJson model.generate(prompt); context.put(analysisPlan, rawJson); context.put(analysisObject, extractField(rawJson, analysisObject)); context.put(timeRange, extractField(rawJson, timeRange)); } private String extractField(String json, String key) { // 简化写法生产环境请使用 Jackson/Gson 解析 String marker \ key \; int idx json.indexOf(marker); if (idx 0) { return ; } int start json.indexOf(:, idx) 1; int end json.indexOf(,, start); if (end 0) { end json.indexOf(}, start); } return json.substring(start, end) .replace(\, ) .trim(); } }这个节点把“模型自由发挥”限制在输出 JSON 结构上后续节点可以拿到稳定的字段。即使模型偶尔输出不规范extractField也能提取出关键信息不至于整条链路崩溃。6.3 知识库检索节点从知识库拿依据知识库检索节点是 RAG检索增强生成的核心。它根据业务分析节点生成的分析对象和时间范围在知识库中检索相关的指标定义、业务规则和分析案例。先准备一个简单的内存知识库存放两三条业务规则。真实项目中可以替换为 Elasticsearch、Milvus、PGVector 等向量检索服务。// 文件路径src/main/java/com/example/agent/node/KnowledgeBaseRetrieverNode.java package com.example.agent.node; import com.example.agent.graph.AgentContext; import com.example.agent.graph.Node; import dev.langchain4j.data.document.Document; import dev.langchain4j.data.document.splitter.DocumentSplitters; import dev.langchain4j.data.segment.TextSegment; import dev.langchain4j.model.embedding.EmbeddingModel; import dev.langchain4j.store.embedding.EmbeddingMatch; import dev.langchain4j.store.embedding.EmbeddingSearchRequest; import dev.langchain4j.store.embedding.EmbeddingSearchResult; import dev.langchain4j.store.embedding.EmbeddingStore; import java.util.List; import java.util.stream.Collectors; public class KnowledgeBaseRetrieverNode implements Node { private final EmbeddingModel embeddingModel; private final EmbeddingStoreTextSegment embeddingStore; public KnowledgeBaseRetrieverNode(EmbeddingModel embeddingModel, EmbeddingStoreTextSegment embeddingStore) { this.embeddingModel embeddingModel; this.embeddingStore embeddingStore; } Override public String name() { return 知识库检索; } Override public void execute(AgentContext context) { String analysisObject context.get(analysisObject, String.class); String timeRange context.get(timeRange, String.class); String query 分析对象 analysisObject 时间范围 timeRange; // 初始化知识库真实项目应该由独立的任务提前完成 initKnowledgeBaseIfNeeded(); var queryEmbedding embeddingModel.embed(query).content(); EmbeddingSearchRequest request EmbeddingSearchRequest.builder() .queryEmbedding(queryEmbedding) .maxResults(3) .minScore(0.6) .build(); EmbeddingSearchResultTextSegment result embeddingStore.search(request); ListEmbeddingMatchTextSegment matches result.matches(); String knowledge matches.stream() .map(match - match.embedded().text()) .collect(Collectors.joining(\n---\n)); context.put(knowledge, knowledge); System.out.println([知识库] 检索到 matches.size() 条知识片段); } private void initKnowledgeBaseIfNeeded() { // 简化演示这里应判断向量库是否为空为空则加载文档并切分 Document salesRule Document.from( 销售额下降常见原因分析1. 新客户签约周期长 2. 老客户流失 3. 折扣力度不足 4. 区域市场波动 ); var segments DocumentSplitters.recursive(100, 50).split(salesRule); // 生产环境需要避免重复入库这里演示时直接入库 embeddingStore.addAll(segments, embeddingModel.embedAll(segments).content()); } }节点把检索到的知识片段写入context的knowledge字段。这部分知识将作为下一节点生成答案时的“事实依据”。6.4 Agent 决策节点基于检索内容生成结论决策节点接收三部分信息用户问题、业务分析计划、知识库检索片段。它被要求只能基于检索到的知识片段作答不能凭记忆编造业务规则。// 文件路径src/main/java/com/example/agent/node/AgentDecisionNode.java package com.example.agent.node; import com.example.agent.graph.AgentContext; import com.example.agent.graph.Node; import dev.langchain4j.model.chat.ChatLanguageModel; public class AgentDecisionNode implements Node { private final ChatLanguageModel model; public AgentDecisionNode(ChatLanguageModel model) { this.model model; } Override public String name() { return Agent决策; } Override public void execute(AgentContext context) { String userQuestion context.get(userQuestion, String.class); String analysisPlan context.get(analysisPlan, String.class); String knowledge context.get(knowledge, String.class); String prompt 你是一名资深业务分析师。请根据下面的分析计划和知识库片段回答用户问题。 注意只能使用知识库片段中的业务规则和依据禁止编造知识库没有的内容。 用户问题 %s 分析计划 %s 知识库片段 %s 请给出结构清晰的结论和下一步建议。 .formatted(userQuestion, analysisPlan, knowledge); String answer model.generate(prompt); context.put(answer, answer); System.out.println([Agent] 最终回答已生成长度 answer.length()); } }6.5 组装图并运行现在把三个节点组装成一个图。本例没有复杂分支但为了演示条件边添加一条规则如果知识库里一条片段都没检索到就走兜底节点直接返回“知识库依据不足请补充资料”。// 文件路径src/main/java/com/example/agent/AgentApplication.java package com.example.agent; import com.example.agent.graph.AgentContext; import com.example.agent.graph.AgentGraph; import com.example.agent.graph.ConditionalEdge; import com.example.agent.llm.LlmFactory; import com.example.agent.node.AgentDecisionNode; import com.example.agent.node.BusinessAnalyzerNode; import com.example.agent.node.KnowledgeBaseRetrieverNode; import dev.langchain4j.model.chat.ChatLanguageModel; import dev.langchain4j.model.embedding.EmbeddingModel; import dev.langchain4j.model.embedding.AllMiniLmEmbeddingModel; import dev.langchain4j.store.embedding.EmbeddingStore; import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore; import dev.langchain4j.data.segment.TextSegment; public class AgentApplication { public static void main(String[] args) { ChatLanguageModel chatModel LlmFactory.create(); EmbeddingModel embeddingModel new AllMiniLmEmbeddingModel(); EmbeddingStoreTextSegment embeddingStore new InMemoryEmbeddingStore(); AgentGraph graph new AgentGraph() .addNode(new BusinessAnalyzerNode(chatModel)) .addNode(new KnowledgeBaseRetrieverNode(embeddingModel, embeddingStore)) .addNode(new AgentDecisionNode(chatModel)) .entry(业务分析) .edge(业务分析, 知识库检索) .edge(知识库检索, Agent决策) .maxSteps(10); AgentContext context new AgentContext(); context.put(userQuestion, 本月华东区销售额为什么比上月下降应该重点跟进哪些客户); AgentContext result graph.run(context); System.out.println( 最终结果 ); System.out.println(result.get(answer, String.class)); } }组装图的方式和写业务代码的体验完全不同你不再关心模型在循环里“想”了什么只关心节点之间的拓扑关系。三个节点的顺序一旦确定整条链路的执行顺序就有了确定性保障。7. 运行验证与效果判断7.1 运行命令确保已经设置环境变量后运行主类export LLM_BASE_URL你的模型服务地址 export LLM_API_KEY你的API Key export LLM_MODEL_NAME你的模型名称 mvn compile exec:java -Dexec.mainClasscom.example.agent.AgentApplication7.2 预期输出控制台应该先输出图执行的节点轨迹[Graph] 执行节点: 业务分析 (step0) [知识库] 检索到 3 条知识片段 [Graph] 执行节点: Agent决策 (step1) 最终结果 基于知识库中的业务规则华东区销售额环比下降可能由以下原因导致……如果看到[Graph] 执行节点依次经过三个节点且最终结果包含知识库中的规则内容说明 Graph 流程已经跑通。7.3 如何判断成功判断标准有三条节点执行顺序正确日志里业务分析必须出现在知识库检索之前Agent 决策必须在最后。知识库检索真实生效检索到 3 条知识片段中的数量符合预期而不是 0。最终答案有事实依据回答内容应该包含知识库中定义的业务规则而不是模型自己临场发挥。7.4 失败时先看哪里如果知识库检索到 0 条优先检查minScore参数。文本数量少时向量相似度普遍偏高但不同嵌入模型的分数分布差异很大0.6 对某些模型可能太严格。可以降低到 0.3 或 0.4 再试。如果第一个节点执行就报错优先检查环境变量是否正确加载。LLM_BASE_URL和LLM_API_KEY缺失时LangChain4j 会抛出创建模型时的配置异常。如果图没有在预期节点结束检查maxSteps是否设置合理以及条件边是否意外命中。8. 常见问题与排查思路问题现象可能原因排查方式解决方案启动时报模型服务地址错误环境变量未设置或 base-url 配置有误打印环境变量值检查服务地址是否可达修正环境变量通过 curl 验证模型服务连通性业务分析节点输出不规则 JSON模型未严格遵循提示词格式打印原始输出检查是否包含多余文字增加输出格式示例或启用 JSON mode或降低 temperature知识库检索始终返回 0 条检索分数阈值过高、文档未入库查看向量库中文档数量临时调低 minScore调整 minScore或改用 Rerank 策略Agent 回答与知识库内容不一致提示词约束不够强检查最终答案来源在提示词中明确“只允许引用知识库片段编号”或增加结果校验节点图执行超过最大步数条件边没有接住所有分支检查 graph 日志中最后一个执行节点为每个节点添加默认边确保任何分支都有出口生产环境直接使用内存向量库数据量增长后查询性能下降观察请求耗时替换为 Milvus、Elasticsearch 或 PGVector9. 最佳实践与工程建议9.1 节点粒度一个节点只做一件事很多团队刚开始写图时喜欢把“业务分析 检索 决策”写到一个节点里理由是减少代码文件。这种做法的代价是失去图的可观测性当你看到日志里只有一行“执行节点: 综合处理”你没法知道问题出在分析阶段还是检索阶段。更合理的拆分规则是凡是可能独立变更、独立测试、独立复用的逻辑都应该独立成节点。业务分析节点可以单独用固定输入测试输出 JSON 结构知识库检索节点可以单独测试召回质量决策节点可以单独测试模型是否遵守知识库约束。9.2 状态字段要提前约定建议团队为每个业务域定义一份“状态字典”写明每个字段的名称、类型、携带者和消费节点。例如{ userQuestion: String入口写入业务分析节点读取, analysisPlan: String业务分析节点写入Agent决策节点读取, knowledge: String知识库检索节点写入Agent决策节点读取, answer: StringAgent决策节点写入出口读取 }状态字段命名混乱是 Graph 项目后期最大的维护成本。没有字典就会出现在analysisPlan和analysis_plan之间反复切换的低级错误。9.3 超时、重试与熔断Graph 中每一类节点都应该有独立超时配置。模型调用节点通常 30 到 60 秒超时知识库检索节点通常 2 到 5 秒超时业务接口调用节点按照下游系统约定。不要在 Graph 层设置一个统一超时因为“模型超时”和“数据库超时”的风险特征完全不同。重试要有上限且只对幂等操作重试。知识库检索可以重试两次模型调用可以重试一次但要用指数退避写订单、发消息这类写操作绝对不能在 Graph 运行器里盲目重试否则可能造成重复订单。9.4 日志与可观测性图运行时天然是打点的好地方。每个节点执行前后都应该输出结构化日志至少包括节点名称、执行耗时、输入状态的关键字段、输出状态的关键字段。推荐使用 JSON 日志方便接入日志平台做后续分析。更进一步可以在AgentContext上附加一个traceId把同一轮 Graph 执行的所有节点日志串联起来。排错时只需要按 traceId 搜索就能看到问题客户、问题节点的完整路径。9.5 知识库的更新策略嵌入式知识库最大的坑是“旧数据永远不更新”。业务规则变了向量库里的旧规则还在Agent 就会引用过时知识。建议给每条知识片段带上版本号或生效时间。更新知识库时采用“按批次重建索引”而不是增量追改。在检索节点里加入时间过滤只检索当前生效的知识。定期做召回评测用一批真实业务问题回测检索质量。9.6 安全边界Agent 工程化里最容易忽略的是权限边界。知识库检索节点的查询词来自用户问题如果企业知识库里混有敏感数据必须做两件事一是按用户权限过滤检索结果二是提示词中禁止模型透露原始知识片段。对外暴露 Agent 服务时API Key 一定不能下发到前端。模型调用应该在服务端完成前端只能拿到最终答案。如果业务需要 Agent 调用外部工具每个工具调用都应该有独立的鉴权和审计。9.7 从 Loop 到 Graph 的迁移策略已经写了大量 Loop 代码的团队不要试图一次性重构。推荐做法是先把 Loop 中的每个分支改造成独立的“处理函数”输入输出都用同一个上下文对象。再把分支关系改写成 Graph 的边和条件边跑通一条最简单的链路。最后把函数级节点扩展成类级节点补上超时、重试、日志。这个过程的收益是渐进的第一个步骤就能改善代码结构第二个步骤就能获得可观测性第三个步骤才能获得完整的话单编排能力。如果用一句话总结这套思路Agent 工程化不是把模型调用做得更花哨而是把“模型能干什么”和“流程该怎么走”彻底分开。Graph Engineering 提供的就是这层分离的工程框架。建议先拿一个真实业务场景用本文的节点抽象跑通最小闭环再逐步加入条件边、兜底节点和可观测体系。二十个节点以内的 Agent 服务这套自研模式完全够用超过这个体量再考虑引入更成熟的图编排平台。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑