双语言Agent架构:为什么Java编排,Python推理

0 阅读1分钟

上周有个做了8年Java的朋友问我:你转AI了,还用Java吗?

我说,用。不仅用,Java在我整个Agent系统里是编排核心。但LLM调用层,我交给了Python。

他愣了一下:那你不嫌麻烦?两套语言,两套部署,两套配置?

我理解他的困惑。做Java的人天然倾向于"一个技术栈搞定一切"——Spring Boot能调API吧?能发HTTP吧?那LLM不就是个API调用?为什么还要拉Python进来?

但做了两周Agent之后,我的判断很明确:不是Java做不到,是Python生态在LLM层没有平替。而Java在业务编排层,Python同样替代不了。 这不是选边站,是职责分离。

Python在LLM层的三个杀手锏

1. 结构化输出:instructor + Pydantic

Agent调LLM不是拿一段自由文本就完事。我的系统里,选题推荐要返回候选列表+评分+理由,草稿生成要返回大纲+正文+标题候选。这些都是强Schema约束的JSON。

Python有instructor这个库,直接把Pydantic模型绑到LLM调用上:

  • LLM返回的JSON自动校验成Pydantic对象

  • 校验失败自动重试(可配重试次数和策略)

  • 支持嵌套模型、枚举约束、字段描述注入prompt

Java里你要自己写JSON解析 + 校验 + 重试循环,而且LLM输出的JSON格式不稳定是常态——漏字段、类型错、枚举值拼错——这些边界情况instructor帮你全兜了,Java里你得自己兜。

2. 模型路由:litellm

我的Agent需要在不同模型间切换——qwen-plus做日常选题,qwen-max做深度草稿,未来可能接入GPT-4o做质检。litellm统一了调用接口:

  • openai/qwen-plus走阿里云,传openai/gpt-4o走OpenAI

  • 自动处理不同provider的参数差异(streaming、function calling格式)

  • 内置token计数、错误重试、fallback链

  • 一个字符串切换模型,业务代码零改动

Java里没有等价方案。你可以用Spring AI的ChatClient做模型抽象,但在provider路由、自动重试、多模型fallback这块,成熟度和litellm差距明显。

3. 提示工程工具链

Python生态的langchain + instructor + pydantic组合,做提示工程非常自然:

  • Prompt模板管理 + 变量注入

  • Few-shot样例绑定到Pydantic模型

  • 输出格式约束(JSON Schema → Pydantic → 自动校验)

Spring AI在追赶,PromptTemplate + BeanOutputConverter已经有基础能力,但在结构化输出的易用性和可靠性上,Python生态领先至少一个身位。

一句话:Python在"跟LLM对话"这件事上,工具链优势是碾压级的。不是Java做不到,是你要自己造的轮子太多,而且造出来还不圆。

Java在业务编排层的三个硬实力

但反过来,为什么不让Python把活全干了?

1. 有状态业务流程编排

一次内容生成要经过:接收请求→校验→鉴权→查人设→调Python选题→调Python生成→RAG检索→质检→拼装响应→记审计。每一步都有业务逻辑,每一步都可能回滚或降级。

Spring Boot的依赖注入、事务管理、异常体系,处理这种流程编排比FastAPI成熟得多。尤其是"草稿→审核→确认→发布"这种多步骤流程,Java的状态管理是Python难以替代的。

2. 类型安全与配置管理

6年Java做下来的直觉:能在编译期发现的问题,绝不留到运行期。DTO字段名、接口参数、枚举值——Java的类型系统帮你在编译期就拦住不一致。Python的动态类型在LLM调用层是优势(灵活适配各种模型输出),但在业务层是负债(字段名拼错、类型不匹配,运行时才爆)。

3. 团队经验复用

这是最现实的原因。我6年Java经验,做业务编排闭着眼写。用Python写编排层,光是依赖注入和配置管理就要踩一堆坑。技术选型不只看语言能力,还要看团队经验。Java团队做Agent,编排层留在Java是最高效的选择。

架构总览

plaintext

┌──────────────────────────────────────────────────────┐
│                    前端 (Vue3)                        │
└──────────────────────┬───────────────────────────────┘
                       │ HTTP
┌──────────────────────▼───────────────────────────────┐
│              Java 编排层 (Spring Boot)                 │
│  ┌──────────┬──────────┬──────────┬───────────────┐  │
│  │ 鉴权     │ 参数校验  │ 人设查询  │ 流程编排       │  │
│  │ 权限控制  │ DTO约束   │ 配置管理  │ 状态管理       │  │
│  └──────────┴──────────┴──────────┴───────────────┘  │
│  ┌──────────┬──────────┬──────────┬───────────────┐  │
│  │ RAG调度   │ 质检     │ 审计日志  │ 发布管理       │  │
│  └──────────┴──────────┴──────────┴───────────────┘  │
└──────────────────────┬───────────────────────────────┘
                       │ HTTP (内部接口)
┌──────────────────────▼───────────────────────────────┐
│             Python 推理层 (FastAPI + litellm)          │
│  ┌──────────┬──────────┬──────────┬───────────────┐  │
│  │ 选题推荐  │ 草稿生成  │ 人设DNA   │ 结构化输出    │  │
│  │ LLM路由   │ 模型切换  │ 分析      │ (instructor)  │  │
│  └──────────┴──────────┴──────────┴───────────────┘  │
│  ┌──────────────────────────────────────────────────┐ │
│  │              litellm (模型路由层)                  │ │
│  │    openai/qwen-plus    openai/qwen-max    ...    │ │
│  └──────────────────────────────────────────────────┘ │
└──────────────────────┬───────────────────────────────┘
                       │
┌──────────────────────▼───────────────────────────────┐
│          Milvus (向量检索) + Redis + MySQL             │
└──────────────────────────────────────────────────────┘

核心原则:Java拥有业务状态,Python拥有模型调用。

Python不存业务数据,不维护session,不做权限判断——它是无状态的计算服务。Java不碰模型调用细节,不指定模型名,不管理token——LLM的全部复杂性封装在Python内部。

职责边界对照

维度

Java编排层

Python推理层

状态

有状态(session、任务队列、审核流程)

无状态(纯计算服务)

鉴权

✅ 拥有

❌ 不关心

LLM调用

❌ 不碰

✅ 拥有

模型选择

❌ 不管

✅ litellm路由

结构化输出

❌ 不处理

✅ instructor校验

RAG调度

✅ 触发检索、拼装结果

✅ 执行向量查询

审计日志

✅ 全链路记录

❌ 只返回结果

配置管理

✅ 人设、平台、发布规则

✅ 模型、API Key、向量库

部署

独立进程

独立进程

扩缩容

按业务并发

按LLM调用QPS

边界清晰,才能各改各的。Java改业务逻辑不影响Python,Python换模型不影响Java。如果边界模糊——Java硬编码模型名,或者Python缓存用户状态——改一边就炸另一边。

双语言架构最大的坑:契约不一致,而且静默失败

双语言架构最可怕的不是"两套代码要维护",而是两边对同一件事的理解不一样,但系统不会报错

我踩过一个典型案例:Python侧的LLM调用配置和Java侧的配置变量名不一致。Java配了一个名字,Python认的是另一个名字。两边都没报错——Python侧LLM调用失败后,自动回退到启发式逻辑继续跑,输出了看起来"还行"的结果。

我用了好几天才发现LLM根本没被调通,选题推荐一直是硬编码在兜底。

这种问题在单体应用里不可能发生。 同一个进程、同一份配置,变量名不对编译就报错。但双语言架构里,Java和Python各管各的配置,两边的"契约"全靠人肉对齐。对不齐的时候,系统不会红警,只会悄悄降级。

这暴露了双语言架构的一个本质问题:Agent系统的失败模式是"静默降级",不是"显式报错"。 LLM调用失败 → 回退启发式 → 输出看着还行 → 你以为链路通了其实没通。传统微服务里接口挂了返回500,你立刻知道。Agent系统里,失败被静默消化了。

我的解法:在链路里加可观测性标记。 每次生成的结果里带一个usedLlm字段——true表示LLM真正参与了,false表示回退兜底。这样不用看日志,光看输出就知道链路是否通畅。比任何自动化测试都直观。

契约管理的长期解法

目前这块还是靠人肉保证的。我在思考的长期解法:

  1. 接口契约文件化 — Java和Python共享一份接口定义(JSON Schema或OpenAPI),两边从同一份定义生成DTO/Model

  2. CI校验 — 每次MR自动比对Java DTO和Python Pydantic Model的字段一致性,不一致就红灯

  3. 集成测试 — 端到端测试不只校验"输出对不对",还校验"LLM是否真正被调用了"(检查usedLlm标记)

第3点最关键——传统的集成测试只测"功能对不对",Agent系统的集成测试还要测"链路有没有被静默降级"。

给Java同行的建议

如果你也是Java背景在做Agent:

不要试图用Java做所有事。 LLM生态在Python,硬用Java做结构化输出和模型路由,你会在工具链上花比业务逻辑更多的时间。不如把LLM调用层交给Python,Java做编排——你6年的Spring Boot经验在编排层是碾压级优势。

但也不要全交给Python。 业务编排、权限控制、状态管理——这些是Java的主场。Python做这些事你要重新学一套最佳实践,而且你团队的Java经验在编排层可以直接复用。

从第一天就把契约管理做好。 双语言架构的失败通常不是因为"某个语言不行",而是"两边对接口的理解不一致"。建立明确的接口契约,比事后补文档有效一百倍。

在链路里加可观测性标记。 任何可能静默降级的地方,都要有标记让外部能感知到。usedLlm这种字段比任何日志都有用——你一眼就知道这次生成是LLM做的还是兜底逻辑。

关注我,少走3个月弯路