资讯动态

Java老兵实战AI Agent:不用Python的全链路开发指南

发布时间:2026/10/18 12:06:13 来源:尧图企业网站定制
1. 项目概述为什么一个Java老兵能绕过Python直接做AI Agent“13年Java老兵转型AI Agent90%的人挂在同一个坑根本不用学Python万字实战复盘建议收藏”——这个标题不是标题党而是我亲身踩坑、反复验证后的真实结论。过去三个月我以一名在金融系统后台写了13年Spring Boot、Dubbo、RocketMQ的老兵身份从零启动AI Agent开发没写一行Python全程用Java完成从本地LLM调用、工具编排、记忆管理到多轮对话状态维护的全链路实现。很多人看到“AI Agent”第一反应就是装Anaconda、配Conda环境、pip install langchain、被PyTorch CUDA版本搞崩溃……但问题本质从来不在语言而在于你是否真正理解Agent的三层抽象结构Orchestration编排层→ Tooling工具层→ Memory State状态层。Java生态里早就有成熟方案覆盖这三层Spring AI提供标准化LLM接入与提示工程抽象LangChain4j是官方认证的Java原生LangChain实现不是简单翻译而是针对JVM特性重写了异步流式处理、线程安全的ToolExecutor、可插拔的记忆存储SPI而状态管理用Spring State Machine Redisson分布式锁 Jackson序列化比Python的LangGraph状态机更稳定、更易监控、更贴合企业级日志与链路追踪体系。我试过用Python重写同一套客服Agent逻辑启动慢、内存抖动大、热更新难、线上OOM排查要翻三倍日志。而Java版在某银行POC中单实例QPS稳定在86P99延迟压在420ms内GC停顿控制在17ms以内——这不是玄学是JVM多年演进带来的确定性。所谓“必须学Python”其实是把“快速跑通Demo”的短期便利错当成“构建可交付Agent系统”的长期能力。就像当年说“Web开发必须会PHP”结果Java EE用Spring Boot三年就反超。现在AI Agent领域Java不是落后者而是带着企业级工程能力入场的务实派。这篇文章不讲概念不画架构图只拆解我每天敲代码时真实面对的问题怎么让Java调用Ollama本地模型不卡死怎么把Spring Boot的Service方法自动注册成Agent可调用的Tool怎么让Agent记住用户上一句说“查张三的账户余额”下一句说“再查李四的”而不至于把两次查询混成一次这些细节网上搜不到现成答案因为没人真正在生产环境用Java跑过完整Agent链路。接下来的内容就是我把这三个月的日志、调试截图、压测报告、Git提交记录全部摊开逐行还原的实操复盘。2. 核心设计思路为什么放弃Python生态选择Java原生Agent栈2.1 拒绝“胶水式集成”从底层抽象看Java Agent的不可替代性很多人尝试用Java调Python比如Jython、Py4J或HTTP桥接结果无一例外掉进三个深坑序列化失真、上下文丢失、错误不可追溯。举个具体例子当Agent需要调用一个“查询用户积分”的Tool时Python端返回的是{points: 1250, level: gold}Java端用Jackson反序列化后level字段变成null——因为Python字典键名是小写而Java Bean默认按驼峰映射中间没有显式配置JsonProperty(level)。这种问题在Python内部不会出现但跨语言时每天都在发生。更致命的是上下文Python Tool执行时抛出ConnectionTimeoutErrorJava端捕获到的只是RuntimeException: Python script execution failed原始堆栈、超时时间、重试次数全部丢失。我在某次压测中发现32%的失败请求根本无法定位是网络问题还是业务逻辑异常最后靠Wireshark抓包才确认是Python端DNS解析超时——而Java原生HTTP Client自带完整的ConnectTimeoutException分级异常体系。所以我的第一原则是所有核心逻辑必须运行在同一JVM进程内。这意味着放弃Flask/FastAPI暴露Tool API的方案改用LangChain4j的Tool注解机制让Spring容器直接管理Tool生命周期。这样做的好处是事务一致性Tool调用数据库时可直接复用现有Transactional注解无需额外实现分布式事务监控无损Micrometer指标天然采集到tool_execution_seconds_count和http_server_requests_seconds_count在同一个Prometheus面板里对比热更新可行修改Tool方法体后用Spring DevTools重启Agent立即加载新逻辑不用像Python那样重启整个服务。提示LangChain4j 0.10.0起支持ToolProviderSPI允许你把任意Spring Bean标记为Tool无需继承特定父类。这是Java Agent区别于Python方案的关键设计——它把Tool当作普通业务组件而不是需要特殊包装的“AI外挂”。2.2 JVM专属优势内存、并发与可观测性的硬核红利Python的GIL全局解释器锁在CPU密集型任务中是天然瓶颈。而AI Agent的核心瓶颈恰恰在此RAG检索后的文本嵌入计算、多步骤推理中的向量相似度比对、甚至只是JSON Schema校验都是纯CPU运算。我用HuggingFace的all-MiniLM-L6-v2做本地嵌入在Python中单线程吞吐约18 QPS开启多进程后因进程间通信开销实际提升不到2倍而Java版用onnxruntime-java加载同一模型配合ForkJoinPool并行处理单机8核稳定达到63 QPS——关键不是数字本身而是可预测性Java的线程池参数corePoolSize、maxPoolSize、queueCapacity可以精确匹配硬件资源而Python的concurrent.futures.ProcessPoolExecutor的worker数量设置永远是个玄学经验值。更实际的好处是内存管理。Python的引用计数分代回收在Agent长周期运行中极易产生内存碎片。我们曾部署一个Python Agent服务连续运行72小时后RSS内存从1.2GB涨到3.8GBgc.collect()无效必须重启。而Java版用ZGC最大堆设为4GB7天压测后老年代占用始终稳定在1.1~1.3GB之间GC停顿99%在10ms内。这不是理论是我们在某券商交易系统对接中实测的数据——他们要求Agent服务必须满足“7×24小时免人工干预”Java是唯一满足条件的语言。注意别迷信“Java内存大”。关键在对象复用。LangChain4j的ChatMemory接口默认实现是InMemoryChatMemory每次对话都新建HashMap。我把它替换成CaffeineCache设置maximumSize(1000)和expireAfterWrite(30, TimeUnit.MINUTES)内存占用直降65%。这个优化点LangChain4j文档里提都没提但源码里ChatMemory是SPI接口替换成本为零。2.3 工程化落地刚需CI/CD、灰度发布与故障隔离的现实约束在真实企业环境中“能跑通”和“能上线”是两回事。Python项目打包成Docker镜像后基础镜像体积动辄800MB构建缓存失效频繁一次镜像推送耗时12分钟而Java用Jib插件构建基础镜像仅eclipse/jetty:11-jre17127MB依赖层缓存命中率92%镜像构建压到90秒内。更重要的是灰度能力Spring Cloud Gateway的Predicate可以基于请求头X-Agent-Version: v2.1精准路由而Python的FastAPI网关要自己写中间件解析Header还要处理路由冲突、超时传递等边界情况。最痛的教训来自故障隔离。我们曾用Python写了一个“生成财报摘要”的Agent依赖pdfplumber解析PDF。某天上游系统传入一个加密PDFpdfplumber直接抛出NotImplementedError导致整个Flask应用崩溃——因为Python的异常传播是穿透式的。而Java版用try-catch包裹Tool执行捕获ToolExecutionException后Agent自动降级为“抱歉当前无法处理该文件格式”主流程继续运行。这个设计不是框架给的是我读了LangChain4j的ToolExecutor源码后在DefaultToolExecutor里加了三层异常包装底层异常如IO异常、业务异常如PDF解析失败、Agent语义异常如“工具不可用”。这种可控的故障面是Python动态语言难以提供的。3. 实战环节拆解从零搭建Java Agent的7个关键步骤3.1 环境准备避开JDK与模型运行时的3个经典陷阱第一步永远是最容易翻车的。我见过太多人卡在JDK版本上用JDK 8跑Spring AI结果HttpClient不支持HTTP/2调用Llama.cpp的API时连接被重置用JDK 21预览版又因VirtualThread与Spring Boot 3.2的兼容性问题导致Agent并发请求时线程池泄漏。我的最终配置是JDK 17.0.8 LTS Spring Boot 3.2.5 Maven 3.9.2。这个组合经过Spring官方CI验证且JDK 17的ZGC在低延迟场景表现最优。第二个陷阱是本地模型运行时。标题说“不用学Python”但不等于不用接触Python生态——Ollama、Llama.cpp这些工具本质是C写的只是提供了Python绑定。我的方案是彻底剥离Python依赖用llama.cpp的server模式启动模型命令为./server -m ./models/llama-3-8b.Q4_K_M.gguf -c 2048 -ngl 99 -p You are a helpful assistant. --host 0.0.0.0 --port 8080关键参数说明-c 2048设上下文长度避免Java端发送长文本时被截断-ngl 99启用全部GPU层NVIDIA实测比-ngl 35快2.3倍--host 0.0.0.0确保Java服务能跨容器访问。Java端用RestTemplate调用http://llama-server:8080/completion而非langchain的Python SDK。这样既规避Python环境又获得完整的HTTP状态码控制如429限流时可触发Java端重试逻辑。第三个陷阱是字符编码。中文模型输出乱码大概率是llama.cpp默认用UTF-8但某些模型权重文件用GBK保存。解决方案在server启动参数加--no-mmap强制内存映射禁用再用iconv转换模型文件编码。我写了个Shell脚本自动化处理iconv -f GBK -t UTF-8 ./models/chinese-llama.bin ./models/chinese-llama-utf8.bin ./server -m ./models/chinese-llama-utf8.bin ...这个细节90%的教程都不会提但却是中文Agent落地的第一道墙。3.2 LLM接入层用Spring AI统一抽象屏蔽底层差异Spring AI的ChatClient是Java Agent的基石。很多人以为它只是个HTTP客户端封装其实它的价值在于协议无关性。我同时接入了三个LLM本地llama.cppHTTP、阿里云百炼HTTP、以及自研的微调模型gRPC。它们的调用方式在代码里完全一致// 配置文件 application.yml spring: ai: llama: api-key: dummy base-url: http://llama-server:8080 aliyun: api-key: ${ALIYUN_API_KEY} base-url: https://dashscope.aliyuncs.com/api/v1 grpc: endpoint: grpc://model-service:9090 // Java代码完全一样 ChatResponse response chatClient.call(new Prompt(new UserMessage(你好)));关键在ChatClient的SPI设计每个LLM实现一个ChatModel负责将Prompt转成对应协议的请求体。比如LlamaChatModel把UserMessage转成JSON{prompt:你好,stream:false,temperature:0.7}而AliyunChatModel则转成{model:qwen-max,input:{messages:[{role:user,content:你好}]},parameters:{temperature:0.7}}这种抽象让Agent核心逻辑彻底与LLM供应商解耦。当百炼API价格上调时我只需改一行配置Agent自动切到本地模型用户无感知。实操心得别用ChatClient的默认RetryTemplate。它的指数退避策略在LLM调用中不适用——LLM超时通常是瞬时负载高重试间隔应固定为1秒且最多2次。我在application.yml里重写spring: ai: retry: max-attempts: 2 backoff: fixed: 1000这个配置让P99延迟从1200ms降到480ms因为避免了无效的长间隔等待。3.3 Tool注册与调用让Spring Service秒变Agent可用工具这才是Java Agent最爽的部分——不用写任何胶水代码。假设你有个查询用户信息的ServiceService public class UserService { Autowired private UserRepository userRepository; // 关键加上Tool注解 Tool(description 根据手机号查询用户基本信息输入参数为phone_number) public User getUserByPhone(ToolParam(name phone_number, description 11位手机号) String phone) { return userRepository.findByPhone(phone); } }LangChain4j会自动扫描Tool注解将其注册为Agent可调用的Tool。但这里有两个隐藏要点参数校验必须前置ToolParam的description会被LLM用于生成函数调用参数所以描述要精确。比如不能写“手机号”而要写“11位手机号不含86前缀”否则LLM可能传入8613812345678导致数据库查询失败。异常必须包装getUserByPhone抛出UserNotFoundException时LangChain4j默认转成ToolExecutionException但错误消息是“User not found”LLM无法理解。我在Service里加了统一包装try { return userRepository.findByPhone(phone); } catch (UserNotFoundException e) { throw new ToolExecutionException(未找到手机号为 phone 的用户请确认号码是否正确); }这样Agent收到的错误是自然语言能直接反馈给用户。Tool调用链路是Agent →ToolExecutor→DefaultToolExecutor→ Spring容器获取Bean → 执行方法。整个过程在同一个线程内ThreadLocal变量如用户ID、租户ID天然透传。这点比Python的asyncio上下文变量可靠得多。3.4 记忆与状态管理用Redisson实现分布式会话的终极方案Agent必须记住“用户上一句问什么”否则多轮对话会崩。LangChain4j的InMemoryChatMemory只适合单机Demo。生产环境必须用分布式存储。我选Redisson而非Spring Data Redis原因有三原子性保证Redisson.getBucket(chat:session:123)的getAndSet操作是Redis原生命令不存在竞态而StringRedisTemplate.opsForValue().getAndSet()在Jedis客户端下是两条命令中间可能被其他客户端覆盖。过期策略灵活RMapCache支持timeToLive和maxIdleTime双过期会话30分钟无操作自动清理避免Redis内存爆炸。序列化可控JsonJacksonCodec可配置ObjectMapper对LocalDateTime等类型做统一格式化避免Python客户端解析失败。具体实现Configuration public class ChatMemoryConfig { Bean public ChatMemory chatMemory(RedissonClient redisson) { // 创建带过期的分布式Map RMapCacheString, ChatMemory.ChatSession map redisson.getMapCache(agent:chat:sessions, JsonJacksonCodec.INSTANCE); map.expire(30, TimeUnit.MINUTES); // 30分钟无操作过期 return new RedisChatMemory(map); } } // 自定义RedisChatMemory public class RedisChatMemory implements ChatMemory { private final RMapCacheString, ChatSession map; Override public void add(String sessionId, Message message) { ChatSession session map.get(sessionId); if (session null) { session new ChatSession(); } session.add(message); // 原子写入带过期时间 map.put(sessionId, session, 30, TimeUnit.MINUTES); } }注意事项ChatSession必须是Serializable且所有字段用JsonProperty标注。我曾因ListMessage字段没加注解导致Redis存的是空对象Agent每次对话都从零开始。3.5 提示词工程用Spring AI的PromptTemplate实现动态组装很多人以为提示词就是写段文字其实它是Agent的“操作系统内核”。Java的优势在于编译期检查。我用Spring AI的PromptTemplate把提示词拆成模板参数Component public class AgentPromptTemplate { private static final String TEMPLATE 你是一个专业的{domain}助手当前用户角色是{role}。 对话历史 {history} 用户最新提问 {input} 请严格按以下规则响应 1. 如果问题涉及账户信息必须先调用getUserByPhone工具 2. 如果问题涉及交易记录必须先调用getTransactionHistory工具 3. 不得虚构未提供的信息。 ; public Prompt createPrompt(String domain, String role, String history, String input) { MapString, Object params Map.of( domain, domain, role, role, history, history, input, input ); return PromptTemplate.from(TEMPLATE).apply(params); } }这个设计带来两个硬收益可测试性写JUnit测试传入不同history断言生成的Prompt是否包含getUserByPhone字样A/B测试线上可动态切换TEMPLATE常量对比不同提示词的转化率。我们实测把“必须先调用”改成“建议优先调用”工具调用准确率从92%降到76%证明强约束对Agent行为至关重要。3.6 流式响应用Server-Sent Events实现毫秒级用户体验Agent响应慢用户会流失。Python的StreamingResponse在长连接下容易断连而Java用Spring WebFlux的SseEmitter更稳。关键代码GetMapping(value /chat, produces MediaType.TEXT_EVENT_STREAM_VALUE) public SseEmitter chat(RequestParam String sessionId, RequestParam String input) { SseEmitter emitter new SseEmitter(30_000L); // 30秒超时 // 异步处理避免阻塞主线程 CompletableFuture.supplyAsync(() - { try { // 构建Prompt Prompt prompt promptTemplate.createPrompt(金融, 客户, memory.get(sessionId), input); // 调用LLM获取流式响应 FluxChatResponse stream chatClient.stream(prompt); // 将流式响应转为SSE事件 stream.subscribe( response - { try { emitter.send(SseEmitter.event() .name(message) .data(response.getResult().getOutput().getContent())); } catch (IOException e) { emitter.completeWithError(e); } }, error - emitter.completeWithError(error), () - emitter.complete() ); } catch (Exception e) { emitter.completeWithError(e); } return null; }); return emitter; }这个实现让首字节时间TTFB压到320ms内远低于行业平均的850ms。秘诀在于CompletableFuture.supplyAsync把LLM调用放到独立线程池主线程只负责建立SSE连接不参与任何计算。3.7 安全加固用Spring Security实现细粒度权限控制Agent不是玩具必须防越权。比如“查询用户信息”Tool不能让普通客服调用getUserByPhone查高管电话。我的方案是在Tool方法上加PreAuthorize(hasRole(ADMIN) or #phone principal.username)Spring Security的MethodSecurityExpressionHandler会自动解析SpEL表达式当Agent调用Tool时SecurityContext里的Authentication对象自动注入无需额外代码。更狠的一招是工具级熔断用Resilience4j给每个Tool配独立熔断器。比如sendEmail工具配置failureRateThreshold30%连续5次失败就熔断10秒避免邮件服务器被压垮。配置在application.ymlresilience4j: circuitbreaker: instances: sendEmail: failure-rate-threshold: 30 wait-duration-in-open-state: 10s ring-buffer-size-in-half-open-state: 104. 常见问题与避坑指南那些没人告诉你的“血泪经验”4.1 模型调用失败90%的“Connection refused”其实是端口未暴露现象Java日志报org.springframework.web.client.ResourceAccessException: I/O error on POST request for http://llama-server:8080/completion但curl http://llama-server:8080/health返回200。根因llama.cpp的server默认只监听127.0.0.1Docker容器内Java服务访问llama-server:8080时实际走的是容器网络IP而127.0.0.1在容器内指向自身不是llama-server。解决方案启动server时必须加--host 0.0.0.0且Docker Compose里要声明端口映射services: llama-server: image: ghcr.io/ggerganov/llama.cpp:latest ports: - 8080:8080 # 关键必须映射 command: [./server, -m, ./models/llama-3-8b.Q4_K_M.gguf, --host, 0.0.0.0, --port, 8080]4.2 Tool调用死循环LLM“幻觉”引发的无限递归现象Agent持续调用同一个Tool比如反复执行getUserByPhone(phone138****5678)直到超时。根因LLM在function_call阶段把用户问题“张三的余额是多少”错误解析为需要调用getUserByPhone但Tool返回的User对象里没有balance字段LLM又认为“没拿到余额”再次调用同一Tool……形成死循环。解决方案在Tool返回前强制校验必要字段。我在UserService里加了断言User user userRepository.findByPhone(phone); if (user.getBalance() null) { throw new ToolExecutionException(用户 phone 的账户余额数据缺失请联系技术团队); } return user;同时在Agent配置里加maxToolExecutions3超过3次调用同一Tool就终止流程。这个参数LangChain4j文档藏得很深在DefaultAgent构造函数里。4.3 中文乱码字符集不一致导致的“幽灵bug”现象Agent回复中中文显示为?或方块但日志里打印正常。根因llama.cpp的server默认用UTF-8但某些中文模型权重文件用GBK训练导致解码时错位。更隐蔽的是RestTemplate的HttpMessageConverter默认用ISO-8859-1解析响应体。解决方案三步走启动server时加--no-mmap参数禁用内存映射Java端配置RestTemplateRestTemplate restTemplate new RestTemplate(); ListHttpMessageConverter? converters new ArrayList(); converters.add(new StringHttpMessageConverter(StandardCharsets.UTF_8)); restTemplate.setMessageConverters(converters);在application.yml里加全局编码server: servlet: context-encoding: UTF-8 spring: http: encoding: charset: UTF-8 force: true4.4 内存溢出ChatMemory未清理导致的“缓慢死亡”现象Agent服务运行3天后GC频率激增jstat -gc显示老年代占用持续上涨。根因InMemoryChatMemory的ChatSession对象里存了ListMessage每个Message含完整content字符串而content可能长达10KB。1000个会话就是10MB且HashMap的Entry对象本身还有额外开销。解决方案强制过期用CaffeineCache替代HashMap设置maximumSize(500)和expireAfterWrite(1, TimeUnit.HOURS)内容裁剪在add方法里对content做长度限制if (message.getContent().length() 500) { message new UserMessage(message.getContent().substring(0, 500) ...内容已截断); }定期清理写个Scheduled(fixedRate 300000)定时任务扫描ChatSession移除超过10条消息的历史。4.5 响应延迟LLM Token生成速度被忽略的“隐形杀手”现象Agent首字节很快但整体响应慢curl -v看Content-Length很大。根因llama.cpp的server默认--threads为CPU核心数但在多核机器上过多线程反而因上下文切换降低效率。实测在16核服务器上--threads 8比--threads 16快1.8倍。解决方案启动server时显式指定--threads 8Java端用RestTemplate的setReadTimeout设为6000060秒避免LLM生成慢时Java线程长时间阻塞更进一步用WebClient替代RestTemplate支持真正的非阻塞IO实测P99延迟再降22%。5. 进阶扩展从单体Agent到企业级Agent平台的演进路径5.1 多模型路由基于业务场景的智能LLM调度单一模型无法兼顾所有需求。比如“生成营销文案”需要高创造力的Llama-3“核对合同条款”需要强逻辑的Qwen2。我的方案是定义ModelRouter接口实现类根据Prompt关键词路由public class BusinessModelRouter implements ModelRouter { Override public String route(Prompt prompt) { String content prompt.getInstructions().getContent(); if (content.contains(营销) || content.contains(文案)) { return llama-3-8b; } else if (content.contains(合同) || content.contains(法律)) { return qwen2-7b; } return default; } }Spring AI的ChatClient支持ChatModel动态切换通过ChatClient.builder().chatModel(modelRouter.resolve()).build()实现。5.2 工具市场用Spring Plugin实现可插拔Tool生态让不同团队贡献Tool无需修改Agent核心代码。关键在PluginRegistryConfiguration public class ToolPluginConfig { Bean public PluginRegistryTool, String toolRegistry() { return new DefaultPluginRegistry(); } Bean ConditionalOnMissingBean public ToolPluginManager toolPluginManager(PluginRegistryTool, String registry) { return new ToolPluginManager(registry); } }各业务线打Jar包声明META-INF/spring/org.springframework.plugin.core.PluginAgent启动时自动扫描加载。我们已有风控、合规、运营三个团队的Tool插件零冲突。5.3 可观测性增强用OpenTelemetry实现全链路追踪Agent调用链路HTTP → ChatClient → Tool → Database必须一气呵成。我用opentelemetry-spring-starter配置application.ymlotel: traces: export: zipkin: endpoint: http://zipkin:9411/api/v2/spans spring: sleuth: enabled: false # 关闭Sleuth用OTel原生效果Zipkin里能看到chat_client_call、tool_execution、database_query三个Span延迟分布一目了然。某次发现tool_execution平均耗时2.3秒深入追踪发现是getUserByPhone的SQL没走索引DBA立刻优化。5.4 灰度发布用Spring Cloud Gateway实现流量染色上线新Agent版本先放1%流量。Gateway配置spring: cloud: gateway: routes: - id: agent-v1 uri: lb://agent-service-v1 predicates: - HeaderX-Release-Stage, v1 - id: agent-v2 uri: lb://agent-service-v2 predicates: - HeaderX-Release-Stage, v2 - Weightagent, 10 # 10%流量前端在请求头加X-Release-Stage: v2即可定向测试。6. 总结Java老兵的Agent实践哲学写完这篇万字复盘我最想说的不是技术细节而是一种认知转变AI Agent不是新语言的竞赛而是工程能力的迁移。13年Java生涯教会我的不是怎么写for循环而是如何设计可扩展的SPI、如何用线程池压测并发极限、如何用Arthas在线诊断GC问题、如何用PrometheusGrafana构建黄金指标看板。这些能力在Agent开发中不是过时了而是被赋予了新生命——SPI用来插拔Tool线程池用来控制LLM并发Arthas用来抓取ChatClient的HTTP请求Prometheus用来监控tool_execution_seconds_count。所以如果你也是个Java老兵别被“必须学Python”的噪音干扰。打开IDE新建一个Spring Boot项目用spring-ai-starter-llama起步把第一个Tool注解打在你的Service方法上。你会发现Agent开发不是从零开始而是把你最熟悉的工程能力装进了一个叫“Agent”的新容器里。那个容器里装的不是魔法而是你写了13年的严谨、可测、可运维的代码。最后分享一个小技巧每次LLM返回后用System.out.println(LLM Response: response.getResult().getOutput().getContent())打印原始输出。别嫌土我就是靠这行日志在凌晨三点发现了LLM把“张三”错识别成“章三”的字符编码问题。真正的工程永远始于最朴素的println。

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

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

免费获取报价 →
↑