Java后端从零手写AI Agent:LLMClient、ToolRegistry与AgentExecutor实战
1. 为什么Java后端要自己造一个AI Agent1.1 从“调个接口”到“造个系统”的认知转变很多Java后端第一次接触AI Agent脑子里想的都是“不就是调个大模型接口吗我加个HTTP客户端就完事了”。我一开始也这么想直到真正动手做一个能写进简历的项目才发现完全不是一回事。调接口只是最表层的一步真正难的是让模型能稳定地调用你的业务方法、记住上下文、在多个工具之间做选择、出错之后能重试或者降级。这些东西如果不用一套工程化的结构去组织代码写到第三天就会变成一坨谁也不敢碰的意大利面。所谓AI Agent用大白话讲就是“让大模型自己决定下一步干什么”。你给它一个目标它自己判断是该查数据库、该调某个服务、还是该直接回答。这中间涉及几个核心角色LLMClient负责和模型通信ToolRegistry负责管理所有可被调用的工具AgentExecutor负责驱动整个“思考-行动-观察”的循环。这三个东西构成了一个最小可用的Agent骨架也是我在这个项目里反复打磨的部分。这个项目适合谁如果你是有一定Java基础、想往AI工程方向靠的后端或者你正在准备面试、需要一个能讲二十分钟且有深度的项目那这套东西非常合适。它不需要你有算法背景也不需要你懂模型训练核心考察的是你的工程抽象能力和对业务的理解。1.2 为什么不用现成框架非要自己写市面上确实有现成的Agent框架Python生态里尤其多。但作为Java人直接用Python框架有两个问题一是你的简历上写的是“使用了某某框架”面试官一问底层就露馅二是Java后端的技术栈和Python生态天然割裂你很难把Agent能力无缝嵌进现有的Spring服务体系里。自己从零实现的好处是每一个环节你都能讲清楚为什么这么设计。比如为什么ToolRegistry要用ConcurrentHashMap而不是普通HashMap为什么AgentExecutor的循环要有最大步数限制为什么LLMClient的响应解析要单独抽一层。这些细节在面试里就是区分“用过”和“做过”的关键。而且自己写的代码你可以完全控制它的行为出问题的时候能定位到具体哪一行而不是对着框架的黑盒干瞪眼。我个人的判断是2024年之后Java后端岗位对AI工程能力的要求会越来越普遍。你现在花两周时间把这套东西吃透比刷一百道八股文管用得多。因为它证明的不只是你会写代码而是你能把一个新的技术领域用工程化的方式落地。2. 核心模块拆解LLMClient、ToolRegistry、AgentExecutor2.1 LLMClient不只是发HTTP请求那么简单LLMClient的职责表面上看很简单把用户的消息发给大模型把模型的回复拿回来。但实际做的时候有几个坑必须提前想清楚。第一个坑是消息格式的统一。不同模型提供商的API格式不一样有的用messages数组有的用prompt字符串。如果你直接在业务代码里拼JSON后面换模型的时候会改到崩溃。我的做法是定义一个内部的ChatMessage类包含role和content两个字段然后在LLMClient内部做转换。这样上层代码永远只跟ChatMessage打交道换模型只需要改LLMClient的实现类。第二个坑是超时和重试。大模型的响应时间波动很大有时候两秒就回来了有时候要等十几秒。如果你不设超时线程就一直挂在那里。我的配置是连接超时5秒读取超时60秒重试次数设为2次重试间隔用指数退避。这里要注意重试不能无脑重试如果是因为请求参数错误导致的失败重试多少次都没用所以要根据HTTP状态码来判断是否值得重试。第三个坑是响应解析的容错。模型有时候会返回一些奇怪的格式比如JSON里多了一个逗号或者把该用双引号的地方用了单引号。如果你直接用Jackson或者Gson去反序列化会直接抛异常。我的做法是在解析之前先做一次清洗把常见的格式问题修掉然后再解析。如果解析还是失败就把原始响应记录下来方便排查。public class LLMClient { private final HttpClient httpClient; private final String apiKey; private final String endpoint; private final int maxRetries 2; public String chat(ListChatMessage messages) { String requestBody buildRequestBody(messages); for (int i 0; i maxRetries; i) { try { HttpResponseString response sendRequest(requestBody); if (response.statusCode() 200) { return parseResponse(response.body()); } if (response.statusCode() 500 i maxRetries) { Thread.sleep((long) Math.pow(2, i) * 1000); continue; } throw new LLMException(请求失败状态码 response.statusCode()); } catch (IOException | InterruptedException e) { if (i maxRetries) throw new LLMException(重试耗尽, e); } } throw new LLMException(不可达逻辑); } }这段代码里有个细节值得说Thread.sleep的指数退避用的是Math.pow(2, i) * 1000也就是第一次等1秒第二次等2秒。为什么不用固定间隔因为如果服务端是因为瞬时压力过大导致的失败固定间隔重试很可能再次撞上压力峰值指数退避能给服务端更多恢复时间。2.2 ToolRegistry让模型知道有哪些工具可用ToolRegistry是整个Agent的能力边界。模型本身只会生成文本它之所以能“调用工具”是因为我们在提示词里告诉了它有哪些工具、每个工具接受什么参数、返回什么结果。ToolRegistry要做的就是把这些信息结构化地管理起来。每个工具在我的设计里是一个Tool接口的实现包含三个核心方法getName()返回工具名getDescription()返回工具描述execute(MapString, Object params)执行具体逻辑。工具名和描述会被拼进系统提示词里所以描述写得清不清楚直接决定了模型能不能正确选择工具。这里有个经验工具描述要写得像给新人看的文档。不要写“查询用户信息”要写“根据用户ID查询用户的姓名、邮箱和注册时间参数userId为字符串类型的用户唯一标识”。描述越具体模型选错工具的概率越低。我实测下来把描述从一句话扩展到三句话工具选择的准确率大概能提升百分之二三十。ToolRegistry内部用ConcurrentHashMap来存储工具key是工具名value是Tool实例。为什么用ConcurrentHashMap而不是HashMap因为AgentExecutor可能是多线程执行的虽然大多数时候一个会话是单线程但如果你要做批量任务处理多个Agent实例可能共享同一个ToolRegistry。用ConcurrentHashMap可以避免并发修改的问题而且它的读操作是完全无锁的性能足够好。public class ToolRegistry { private final MapString, Tool tools new ConcurrentHashMap(); public void register(Tool tool) { if (tools.containsKey(tool.getName())) { throw new IllegalArgumentException(工具名重复 tool.getName()); } tools.put(tool.getName(), tool); } public String buildToolPrompt() { StringBuilder sb new StringBuilder(); sb.append(你可以使用以下工具\n); for (Tool tool : tools.values()) { sb.append(- ).append(tool.getName()) .append().append(tool.getDescription()).append(\n); } sb.append(\n如果需要调用工具请按以下JSON格式输出\n); sb.append({\tool\: \工具名\, \params\: {...}}\n); return sb.toString(); } public Tool getTool(String name) { return tools.get(name); } }buildToolPrompt这个方法很关键它把工具列表转换成模型能理解的提示词。注意最后那段JSON格式的说明这是告诉模型“如果你想调工具就按这个格式输出”。模型输出之后AgentExecutor会去解析这个JSON如果解析成功就执行对应的工具如果解析失败就当作普通文本回复。2.3 AgentExecutor驱动整个思考循环AgentExecutor是Agent的大脑它负责把用户输入、工具列表、历史对话组装成提示词发给LLMClient拿到响应后判断是工具调用还是普通回复如果是工具调用就执行工具、把结果追加到对话历史里然后再次调用模型直到模型给出最终回复或者达到最大步数。这个循环的逻辑听起来简单但有几个地方容易出问题。第一个是最大步数限制。如果不限制模型可能会陷入死循环比如反复调用同一个工具、每次都得到相同的结果、然后继续调用。我的设置是最大10步超过就强制终止并返回当前已有的信息。这个数字不是拍脑袋定的我试过5步、10步、15步5步有时候不够完成复杂任务15步又太浪费token10步是一个比较平衡的值。第二个是工具执行异常的处理。如果工具执行抛异常了不能直接把异常堆栈丢给模型那样模型看不懂。我的做法是捕获异常把异常信息转换成一段人类可读的文字比如“工具执行失败数据库连接超时”然后作为观察结果追加到对话里。这样模型知道工具失败了可以选择重试或者换一个工具。第三个是对话历史的管理。随着循环步数增加对话历史会越来越长token消耗也会越来越大。我的策略是保留最近5轮完整对话更早的对话只保留摘要。摘要的生成可以用一个单独的LLM调用也可以用简单的规则比如只保留工具调用的名称和结果状态。这个策略在大多数场景下够用如果任务特别复杂可以适当增加保留轮数。public class AgentExecutor { private final LLMClient llmClient; private final ToolRegistry toolRegistry; private final int maxSteps 10; public String execute(String userInput) { ListChatMessage history new ArrayList(); history.add(new ChatMessage(system, buildSystemPrompt())); history.add(new ChatMessage(user, userInput)); for (int step 0; step maxSteps; step) { String response llmClient.chat(history); history.add(new ChatMessage(assistant, response)); ToolCall toolCall parseToolCall(response); if (toolCall null) { return response; } try { Tool tool toolRegistry.getTool(toolCall.getToolName()); if (tool null) { history.add(new ChatMessage(user, 工具不存在 toolCall.getToolName())); continue; } Object result tool.execute(toolCall.getParams()); history.add(new ChatMessage(user, 工具执行结果 result)); } catch (Exception e) { history.add(new ChatMessage(user, 工具执行失败 e.getMessage())); } } return 已达到最大执行步数当前结果 history.get(history.size() - 1).getContent(); } }parseToolCall这个方法负责从模型响应里提取工具调用信息。模型输出的格式可能不完全符合我们的要求比如JSON外面包了一层markdown代码块或者字段名大小写不一致。我的做法是用正则先提取出JSON部分然后用一个宽松的解析器去解析字段名做大小写不敏感处理。如果实在解析不出来就返回null当作普通文本处理。3. 从零搭建的完整实操流程3.1 环境准备与项目骨架动手之前先把环境理清楚。JDK版本我建议用17因为17是LTS版本而且文本块和record类型对写这种项目很有帮助。构建工具用Maven就行不需要Gradle这个项目的依赖不复杂。核心依赖只有三个HTTP客户端用JDK自带的java.net.http.HttpClientJSON处理用Jackson日志用SLF4J加Logback。为什么不用Spring Boot因为我想让这个项目保持轻量方便你理解每一层在做什么。如果你要把它集成到现有的Spring项目里把LLMClient和ToolRegistry注册成Bean就行AgentExecutor可以用原型作用域每次请求创建一个新实例。项目结构我建议这样组织ai-agent/ ├── pom.xml ├── src/main/java/com/example/agent/ │ ├── llm/ │ │ ├── LLMClient.java │ │ ├── ChatMessage.java │ │ └── LLMException.java │ ├── tool/ │ │ ├── Tool.java │ │ ├── ToolRegistry.java │ │ └── impl/ │ │ ├── CalculatorTool.java │ │ ├── TimeTool.java │ │ └── HttpTool.java │ ├── executor/ │ │ ├── AgentExecutor.java │ │ └── ToolCall.java │ └── Main.java └── src/main/resources/ └── logback.xml这个结构的好处是职责清晰。llm包只管和模型通信tool包只管工具的定义和注册executor包只管循环驱动。你后面要加新的工具只需要在tool/impl下面新建一个类然后在Main里注册一下就行完全不用动executor的代码。3.2 三个内置工具的实现细节为了让Agent有实际能力我实现了三个基础工具计算器、时间查询和HTTP请求。这三个工具覆盖了数值计算、时间获取和外部接口调用三种典型场景足够演示Agent的工作流程。计算器工具用javax.script.ScriptEngine来实现虽然这个API在新版本JDK里被标记为废弃但用来做演示足够了。如果你要上生产建议换成专门的表达式求值库比如exp4j或者mvel。计算器的描述我写的是“计算数学表达式参数expression为字符串类型的数学表达式支持加减乘除和括号”。这里要注意一定要在描述里说明参数名和类型否则模型可能传错参数。时间工具返回当前日期和时间描述里要写清楚返回的格式。我一开始没写格式模型拿到结果之后不知道怎么处理后来加上“返回格式为yyyy-MM-dd HH:mm:ss”就正常了。HTTP工具稍微复杂一点它接受一个URL和一个可选的请求方法返回响应体的前500个字符。为什么要限制500个字符因为如果返回内容太长会占用大量token而且模型可能被无关信息干扰。500个字符足够模型判断请求是否成功、返回了什么关键信息。这个工具在实际使用中要小心不能让模型随意请求内网地址所以我在实现里加了一个白名单校验只允许请求预先配置的域名。public class HttpTool implements Tool { private final SetString allowedHosts; public HttpTool(SetString allowedHosts) { this.allowedHosts allowedHosts; } Override public String getName() { return http_request; } Override public String getDescription() { return 发送HTTP GET请求获取网页内容参数url为字符串类型的完整URL返回响应体前500个字符; } Override public Object execute(MapString, Object params) throws Exception { String url (String) params.get(url); URI uri URI.create(url); if (!allowedHosts.contains(uri.getHost())) { throw new SecurityException(不允许访问该域名 uri.getHost()); } HttpClient client HttpClient.newHttpClient(); HttpRequest request HttpRequest.newBuilder(uri).GET().build(); HttpResponseString response client.send(request, HttpResponse.BodyHandlers.ofString()); String body response.body(); return body.length() 500 ? body.substring(0, 500) ... : body; } }这个白名单机制很重要。如果你不做限制模型可能会被诱导去请求一些不该请求的地址。虽然这只是一个演示项目但养成安全习惯对以后做真实项目有好处。3.3 系统提示词的设计与调优系统提示词是Agent行为的“宪法”它决定了模型怎么理解自己的角色、怎么选择工具、怎么组织回复。我前后改了七八版最后稳定下来的版本包含四个部分角色定义、工具说明、输出格式要求、行为约束。角色定义部分我写的是“你是一个乐于助人的AI助手可以调用工具来帮助用户解决问题。在回答之前先判断是否需要调用工具。如果可以直接回答就直接回答如果需要工具就按格式输出工具调用。”这段话看起来简单但它明确了“先判断再行动”的流程避免了模型一上来就瞎调工具。工具说明部分由ToolRegistry的buildToolPrompt方法动态生成这样加新工具的时候不用改提示词。输出格式要求部分我写了两条规则调用工具时输出JSON直接回答时输出纯文本。这里有个细节我要求JSON必须在一行内完成不要换行。因为换行会导致正则提取变得复杂一行内的JSON更容易解析。行为约束部分我加了三条不要编造工具执行结果、如果工具执行失败要如实告知用户、不要重复调用同一个工具超过两次。这三条都是我在测试中发现问题后加上的。特别是最后一条有一次模型反复调用时间工具每次得到相同结果还继续调加了这条之后就正常了。提示系统提示词不要写得太长控制在500字以内。太长的提示词会占用大量token而且模型可能抓不住重点。如果确实需要更多约束可以考虑用few-shot示例的方式给一两个正确调用的例子比纯文字描述效果好。4. 实测中踩过的坑与排查技巧4.1 模型不按格式输出怎么办这是最常见的问题。你明明在提示词里写了“按JSON格式输出”但模型就是给你返回一段自然语言比如“好的我来帮你计算一下调用计算器工具表达式是11”。这种情况有两种处理思路。第一种是加强格式约束。在提示词里把格式要求写得更死比如“你的输出必须且只能是一个JSON对象不要包含任何其他文字”。同时可以在提示词末尾加一句“如果输出不符合格式系统将无法执行你的请求”。我实测下来加上这句话之后格式错误的概率大概降低了一半。第二种是做兼容解析。如果模型返回的是自然语言但里面包含了工具名和参数信息可以尝试用正则去提取。比如从“调用计算器工具表达式是11”里提取出工具名“计算器”和表达式“11”。这种方式的缺点是规则会越写越多维护成本高。我的建议是两者结合先用严格格式约束同时保留一个简单的兼容解析作为兜底。如果格式错误频繁发生还有一个可能是模型本身的能力问题。不同模型对格式指令的遵循程度差异很大。如果你用的是较小的模型可能需要换一个更大的模型或者在提示词里给一个完整的输出示例。4.2 工具调用参数类型不匹配模型输出的JSON里参数类型经常和预期不一致。比如计算器工具期望expression是字符串但模型可能输出{expression: 11}这里的11不是合法的JSON值。或者时间工具不需要参数但模型硬塞了一个{format: yyyy-MM-dd}进来。处理这个问题我的做法是在Tool接口的execute方法里做参数校验和转换。对于字符串参数如果传进来的是数字就自动转成字符串。对于不需要参数的工具忽略多余的参数。对于必须的参数如果缺失就抛出明确的异常信息让模型知道缺了什么。public class CalculatorTool implements Tool { Override public Object execute(MapString, Object params) { Object exprObj params.get(expression); if (exprObj null) { throw new IllegalArgumentException(缺少必需参数expression); } String expression String.valueOf(exprObj); // 后续计算逻辑 } }String.valueOf这个方法很实用它能把任何对象转成字符串包括数字、布尔值等。这样即使模型传了数字类型的参数也能正常处理。4.3 循环不终止的排查思路AgentExecutor的循环不终止通常有三个原因。第一个是模型反复调用同一个工具每次得到相同结果但就是不给出最终回复。第二个是工具执行一直失败模型一直重试。第三个是模型输出的JSON解析一直失败被当作普通文本处理但文本里又包含了工具调用的意图导致下一轮又尝试解析。排查的时候我建议先把日志打全。在每一轮循环开始时打印当前的步数、模型原始响应、解析出的工具调用信息。这样你能清楚地看到循环卡在哪一步。如果是第一种原因可以在提示词里加“如果已经调用过某个工具并得到结果不要再重复调用”。如果是第二种原因检查工具本身的实现是否有bug。如果是第三种原因优化解析逻辑或者加强格式约束。最大步数限制是最后的保险。即使前面所有措施都失效10步之后也会强制终止。终止时返回的信息要包含已经收集到的所有工具执行结果这样至少用户能看到部分进展。4.4 常见问题速查表问题现象可能原因排查方法解决方案模型不输出JSON提示词格式约束不够强检查提示词中格式说明是否明确加强格式约束增加输出示例工具名识别错误工具描述不清晰对比模型输出的工具名和注册的工具名优化工具描述使用更具体的名称参数缺失或类型错误模型对参数理解有偏差打印模型原始输出和解析后的参数在execute方法中做参数校验和转换循环不终止模型重复调用或解析失败打印每轮循环的详细日志加最大步数限制优化提示词和解析逻辑响应时间过长模型本身推理慢或网络延迟分别测试LLMClient和工具执行的耗时设置合理的超时时间考虑流式输出token消耗过大对话历史太长统计每轮请求的token数限制历史保留轮数对早期对话做摘要这张表里的每一条都是我实际遇到过的。特别是“工具名识别错误”这一条我一开始把工具名起成“calc”模型经常输出“calculator”或者“计算器”。后来改成“calculator”并加上中文描述问题就解决了。工具名最好用英文小写加下划线描述里可以中英文混用这样模型理解起来最准确。5. 怎么把这个项目写进简历并应对面试5.1 简历描述的写法简历上不要写“使用Java实现了一个AI Agent”太笼统了。要写出技术选型、核心模块和量化结果。我建议的写法是“基于Java 17从零实现轻量级AI Agent框架包含LLMClient通信层、ToolRegistry工具注册中心和AgentExecutor循环执行器支持多轮工具调用与异常降级单次任务平均执行步数3.2步工具调用准确率92%。”这段话里有几个关键点从零实现说明你不是调包三个核心模块说明你有架构能力多轮工具调用说明你理解了Agent的本质量化数据说明你做过测试和调优。面试官看到这样的描述大概率会追问细节而你已经准备好了。如果你把这个项目集成到了现有的Spring项目里还可以加一句“已集成至公司内部运维平台支持自然语言查询服务器状态和日志检索”。这样就从个人项目变成了有实际业务价值的项目含金量更高。5.2 面试中可能被追问的问题面试官对这个项目的追问通常集中在三个方向架构设计、异常处理和性能优化。架构设计方面常见的问题是“为什么ToolRegistry要用ConcurrentHashMap”和“AgentExecutor的循环为什么要有最大步数限制”。这两个问题我在前面都详细解释过了核心思路是并发安全和防止死循环。异常处理方面面试官可能会问“如果模型返回的JSON格式不对怎么办”和“如果工具执行超时怎么办”。我的回答思路是分层处理格式问题在解析层做兼容和清洗超时问题在工具执行层做超时控制和降级。性能优化方面可能会问“怎么减少token消耗”和“怎么提高工具调用准确率”。token消耗的优化手段包括限制历史轮数、对早期对话做摘要、精简系统提示词。工具调用准确率的提升手段包括优化工具描述、增加few-shot示例、对模型输出做后处理校验。还有一个高频问题是“你这个Agent和LangChain有什么区别”。我的回答是“LangChain是一个通用框架功能很全但也很重。我这个项目是轻量级的只保留了最核心的三个模块代码量不到一千行更容易理解和定制。而且我是用Java写的可以无缝集成到现有的Java后端体系里。”这个回答既展示了你的技术判断力也解释了为什么不用现成框架。5.3 后续可以扩展的方向这个项目目前只支持文本工具调用后续可以往几个方向扩展。第一个是多模态支持让Agent能处理图片和文件。第二个是记忆机制把对话历史持久化到数据库支持跨会话的上下文。第三个是多Agent协作让多个Agent分工合作完成复杂任务。第四个是可观测性接入监控系统记录每次工具调用的耗时和成功率。这些扩展方向不需要全部实现但你要知道它们的存在。面试的时候如果被问到“这个项目还有什么可以改进的地方”你可以挑一两个方向展开讲展示你的技术视野。注意扩展方向不要贪多选一个你最熟悉的深入讲。比如你选记忆机制就要能讲清楚用什么数据库、表结构怎么设计、怎么处理并发写入。泛泛而谈不如深入一点。6. 一些实操心得和避坑建议6.1 关于模型选择的经验不同模型对工具调用的支持程度差异很大。我测试过几个主流模型有的模型天生就擅长输出结构化内容工具调用准确率很高有的模型则需要反复调提示词才能勉强用。如果你发现格式问题频繁出现先别急着改代码换一个模型试试可能问题就消失了。另外模型的响应时间也很重要。Agent场景下一次任务可能需要多轮模型调用如果每轮都要等十秒用户体验会很差。我建议在项目初期就用一个响应速度较快的模型做开发调试等流程跑通了再考虑换更强的模型。6.2 关于日志和调试的建议Agent的调试比普通程序麻烦因为中间多了模型这个不确定因素。我的做法是把每一轮的输入输出都完整记录下来包括发给模型的提示词、模型的原始响应、解析后的工具调用、工具的执行结果。这些日志在排查问题的时候非常有用。日志级别我建议用DEBUG记录详细内容用INFO记录关键节点。生产环境可以把DEBUG关掉只保留INFO。如果出了线上问题再临时打开DEBUG复现。6.3 关于代码组织的体会这个项目虽然不大但分层一定要清晰。我见过有人把所有逻辑写在一个类里LLM调用、工具执行、循环控制全混在一起后面加一个工具就要改好几个地方。分层的好处是每一层只关心自己的职责LLMClient不关心工具怎么执行ToolRegistry不关心模型怎么调用AgentExecutor不关心HTTP请求怎么发。这样你改任何一层都不会影响其他层。接口的设计也很重要。Tool接口只有三个方法但就是这三个方法支撑起了整个工具生态。你后面加任何工具只要实现这三个方法就行。这种“小接口、多实现”的设计思路在面试里也是一个加分项。6.4 关于测试的策略Agent的测试和普通单元测试不一样因为模型的输出是不确定的。我的做法是分两层测试第一层是单元测试测试LLMClient的请求构建和响应解析、ToolRegistry的注册和查找、各个工具的execute方法这些都不依赖模型可以用Mock对象。第二层是集成测试用真实的模型跑几个典型场景比如“计算11”“现在几点”“帮我查一下某个网页的内容”验证整个流程能跑通。集成测试不需要每次都跑可以在发版前跑一次。单元测试则应该每次提交都跑保证基础逻辑没有回归。6.5 一个容易被忽略的细节工具的执行结果要尽量简洁。我一开始把HTTP请求的完整响应都返回给模型结果token消耗巨大而且模型经常被无关的HTML标签干扰。后来改成只返回前500个字符并且去掉HTML标签只保留纯文本效果好了很多。还有一个细节是工具执行结果的格式。我建议统一用“工具名结果”的格式比如“calculator2”。这样模型能清楚地知道哪个工具返回了什么结果在多工具调用的场景下尤其重要。这个项目我从开始动手到基本稳定大概花了两周左右的业余时间。中间踩了不少坑但每解决一个问题对Agent的理解就深一层。如果你也在做类似的事情我的建议是先把最小闭环跑通哪怕只支持一个工具、只处理最简单的场景然后再逐步扩展。不要一上来就追求大而全那样很容易卡在半路。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →