Python与Java双栈实战:手撸一个支持RAG与Tool Calling的高性能Agent框架
引言
在2026年的AI工程招聘市场上,"会调LangChain"已经不够了。企业真正需要的是能够设计跨语言、高可用、可扩展Agent框架的工程师。为什么?因为生产环境从来不是单一语言的游戏。
Python在AI生态中占据统治地位,LangChain、LlamaIndex等框架让RAG与Tool Calling的快速原型开发变得轻而易举。但当系统需要接入企业级Java微服务、处理高并发请求、保障事务一致性时,纯Python方案开始力不从心。反过来,Java的Spring AI虽然提供了完整的工具调用体系,但在RAG生态的灵活性上仍不及Python。
双栈架构不是"为了双栈而双栈",而是让每种语言做它最擅长的事:Python负责AI编排与检索,Java负责业务权威状态与事务保障。本文将手把手带你用Python + Java构建一个支持RAG与Tool Calling的Agent框架,给出可运行的代码,并解析架构设计中的关键决策。
一、为什么双栈架构是Agent工程化的必然选择
先看一个真实的生产场景:用户对Agent说"帮我查一下上个月的差旅报销状态,如果超过限额就发起申诉流程"。
这个请求涉及三层能力:第一层是知识检索 ("公司差旅报销限额是多少"),第二层是业务查询 ("上个月提交了哪些报销单,状态是什么"),第三层是受控写操作("如果超限,生成申诉Proposal")。
如果全部用Python实现,你会遇到几个问题:Python服务需要直接连接企业数据库,这在安全审计上很难通过;报销状态查询需要遵循Java服务的事务隔离级别,Python重写一套容易引入不一致;最关键的是,当系统需要水平扩展时,Python的异步模型在高并发I/O密集型场景下不如Java的虚拟线程来得稳定。
参考一个成熟的开源企业级项目 enterprise-ai-copilot 的设计,其架构非常清晰:请求先进入Java ,Java负责生成可信的trace_id、employee_id等Runtime Context字段,Python服务只在Docker网络内部暴露端口,不对外映射。Java调用Python做AI编排,Python调用Java的只读业务Tool获取权威状态。
这种设计的核心理念是:Python拥有的是"思考能力",Java拥有的是"事实权威"。
二、架构分层:从MCP到Tool Registry的双栈设计
在动手写代码之前,先明确分层架构。参考AgentCraft项目的五层架构,我们简化为四层:
第一层:协议适配层。由Java的Spring Boot负责,暴露HTTP API给前端,同时提供内部RPC接口供Python调用业务Tool。这一层管理认证、限流、全链路追踪ID。
第二层:Agent编排层。由Python负责,核心是ReAct循环、意图识别、工具路由和RAG检索。这一层不碰任何业务数据库,只通过Tool调用获取事实。
第三层:Tool Registry层。这是双栈架构的"粘合层"。所有工具------无论是Python本地的RAG检索,还是通过MCP协议暴露的Java服务------都注册到统一的ToolRegistry中,使用标准化的Schema描述输入输出。
第四层:能力实现层。Python侧实现向量检索、Embedding、Rerank;Java侧实现业务逻辑、事务、审计。
这个架构的关键在于Tool Registry的抽象设计。Tool不应该关心它是用Python还是Java实现的,它只需要暴露三样东西:name、description、input_schema。
python
# tool_registry.py
from dataclasses import dataclass, field
from typing import Callable, Any, Optional
import inspect
import json
@dataclass
class ToolSchema:
"""工具的输入/输出Schema定义"""
type: str = "object"
properties: dict = field(default_factory=dict)
required: list[str] = field(default_factory=list)
@dataclass
class ToolMetadata:
timeout_seconds: int = 30
max_retries: int = 3
requires_auth: bool = False
@dataclass
class Tool:
name: str
description: str
input_schema: ToolSchema
output_schema: Optional[ToolSchema] = None
metadata: ToolMetadata = field(default_factory=ToolMetadata)
handler: Optional[Callable] = None
is_remote: bool = False # True表示通过MCP/RPC调用Java服务
class ToolRegistry:
"""工具注册器(单例),统一管理本地与远程工具"""
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._tools: dict[str, Tool] = {}
return cls._instance
def register(self, tool: Tool) -> None:
if tool.name in self._tools:
raise ValueError(f"Tool '{tool.name}' already registered")
self._tools[tool.name] = tool
def get_tool(self, name: str) -> Optional[Tool]:
return self._tools.get(name)
def get_all_tools(self) -> dict[str, Tool]:
return self._tools.copy()
def list_tools_for_llm(self) -> list[dict]:
"""转换为OpenAI Function Calling格式"""
tools = []
for tool in self._tools.values():
tools.append({
"type": "function",
"function": {
"name": tool.name,
"description": tool.description,
"parameters": {
"type": tool.input_schema.type,
"properties": tool.input_schema.properties,
"required": tool.input_schema.required
}
}
})
return tools
async def invoke(self, name: str, arguments: dict) -> Any:
"""执行工具调用,带超时和重试"""
tool = self._tools.get(name)
if not tool:
raise ValueError(f"Tool '{name}' not found")
if tool.is_remote:
return await self._invoke_remote(tool, arguments)
# 本地工具直接调用
if asyncio.iscoroutinefunction(tool.handler):
return await asyncio.wait_for(
tool.handler(**arguments),
timeout=tool.metadata.timeout_seconds
)
return tool.handler(**arguments)
这段Registry代码是双栈架构的基石。is_remote标记区分了本地Python工具和远程Java服务,对Agent层完全透明。
三、Python侧:RAG与Agent编排的核心实现
3.1 可插拔的RAG检索器
RAG的核心不是"调一次向量库API",而是检索质量的可控性 。参考llm-agent-base的设计思路,我们需要支持:按相似度阈值过滤弱匹配、支持文件名关键词搜索(不依赖向量索引)、以及增量索引更新。
python
# rag_retriever.py
from dataclasses import dataclass
from typing import Optional
import numpy as np
@dataclass
class Chunk:
content: str
source: str
score: float = 0.0
metadata: dict = None
class RAGRetriever:
"""轻量级RAG检索器,支持向量检索与关键词回退"""
def __init__(self, embedding_fn, vector_store, min_score: float = 0.35):
self.embed = embedding_fn
self.store = vector_store
self.min_score = min_score # 低于此阈值的结果被丢弃
async def retrieve(self, query: str, top_k: int = 5) -> list[Chunk]:
"""语义检索,带质量过滤"""
query_vec = await self.embed(query)
raw_results = await self.store.search(query_vec, top_k=top_k * 2)
# 过滤弱匹配,避免用噪声填满top_k
filtered = [
Chunk(content=r["content"], source=r["source"], score=r["score"])
for r in raw_results
if r["score"] >= self.min_score
]
return filtered[:top_k] if filtered else raw_results[:top_k]
async def keyword_search(self, keywords: list[str],
search_in: str = "both",
match_mode: str = "any",
min_matches: int = 2) -> list[Chunk]:
"""
文件名/内容关键词搜索,无需向量索引
适用场景:结构化文档、代码仓库检索
"""
# 实现省略,核心逻辑:遍历文档索引,匹配文件名或内容
pass
def format_rag_context(chunks: list[Chunk]) -> str:
"""将检索结果格式化为LLM可读的上下文"""
if not chunks:
return "未检索到相关文档。"
parts = []
for i, chunk in enumerate(chunks, 1):
parts.append(f"[文档{i}] 来源: {chunk.source}\n{chunk.content}")
return "\n\n---\n\n".join(parts)
min_score过滤是一个容易被忽视但至关重要的设计。很多RAG实现为了"凑够top_k",把低相关度的内容也塞进上下文,导致LLM被噪声误导。宁可返回更少的上下文,也不要引入幻觉。
3.2 ReAct Agent核心循环
现在实现Agent的执行引擎。核心是一个"推理-行动-观察"的循环,集成RAG检索和Tool Calling。
python
# agent_engine.py
import asyncio
import json
from dataclasses import dataclass, field
from typing import Optional
import openai
@dataclass
class AgentState:
"""Agent执行状态(可序列化,支持持久化)"""
session_id: str
messages: list = field(default_factory=list)
tool_calls_log: list = field(default_factory=list)
iteration_count: int = 0
class ReActAgent:
"""ReAct范式的Agent引擎,支持RAG与Tool Calling混合"""
def __init__(self, llm_client, registry, retriever,
max_iterations: int = 6):
self.llm = llm_client
self.registry = registry
self.retriever = retriever
self.max_iterations = max_iterations
async def run(self, user_input: str, state: AgentState) -> str:
state.messages.append({"role": "user", "content": user_input})
for iteration in range(self.max_iterations):
state.iteration_count = iteration + 1
# 决策:LLM决定是否需要RAG、是否需要调用工具
system_prompt = self._build_system_prompt(state)
tools = self.registry.list_tools_for_llm()
response = await self.llm.chat.completions.create(
model="gpt-4o",
messages=[{"role": "system", "content": system_prompt}] + state.messages,
tools=tools if tools else None,
tool_choice="auto"
)
msg = response.choices[0].message
# 情况一:无工具调用,直接返回最终答案
if not msg.tool_calls:
state.messages.append({"role": "assistant", "content": msg.content})
return msg.content
# 情况二:执行工具调用
state.messages.append(msg)
for tool_call in msg.tool_calls:
tool_name = tool_call.function.name
arguments = json.loads(tool_call.function.arguments)
# 特殊处理:RAG检索作为"虚拟工具"
if tool_name == "retrieve_knowledge":
result = await self._handle_rag_call(arguments)
else:
try:
result = await self.registry.invoke(tool_name, arguments)
except Exception as e:
result = f"[工具调用失败: {str(e)}]"
state.messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": str(result)
})
state.tool_calls_log.append({
"tool": tool_name,
"args": arguments,
"result_preview": str(result)[:200]
})
return "已达到最大迭代次数,任务未完成。"
def _build_system_prompt(self, state: AgentState) -> str:
return """你是一个企业级AI助手。你有以下能力:
1. 通过 retrieve_knowledge 工具检索企业知识库
2. 通过其他工具查询业务系统状态
3. 执行受控的业务操作
重要约束:
- 对于事实性问题(如公司政策、产品信息),必须先调用 retrieve_knowledge 检索
- 对于需要操作业务系统的请求,使用相应的工具
- 如果工具返回结果不充分,可以再次检索或调用其他工具
- 不要编造知识库中不存在的信息"""
async def _handle_rag_call(self, arguments: dict) -> str:
query = arguments.get("query", "")
chunks = await self.retriever.retrieve(query)
return format_rag_context(chunks)
这个Agent循环有几个关键设计:RAG被封装为"虚拟工具" ,与业务工具走同一套调用协议;迭代上限 防止无限循环;状态外置使Agent实例可以无状态部署。
四、Java侧:Tool Provider与MCP服务暴露
Python Agent需要调用Java的业务能力。最优雅的方式是通过MCP协议暴露Java服务,让Agent像调用本地工具一样调用远程Java方法。
参考Spring AI 2.0的MCP支持,Java端只需要一个注解就能将方法暴露为MCP Tool:
java
// WeatherTools.java --- Java侧的业务工具
@Component
public class ExpenseTools {
private final ExpenseService expenseService;
@McpTool(description = "查询员工差旅报销单状态")
public ExpenseStatus queryExpenseStatus(
@McpToolParam(description = "员工工号") String employeeId,
@McpToolParam(description = "月份,格式YYYY-MM") String month) {
return expenseService.queryStatus(employeeId, month);
}
@McpTool(description = "提交差旅报销申诉,返回Proposal ID(不执行实际写操作)")
public ProposalResult submitAppealProposal(
@McpToolParam(description = "报销单ID") String expenseId,
@McpToolParam(description = "申诉理由") String reason) {
// Proposal阶段无副作用,仅生成待确认记录
return expenseService.createProposal(expenseId, reason);
}
}
MCP Server的自动配置会扫描@McpTool注解的方法,生成JSON Schema并注册。Python侧只需要通过MCP Client连接,就能发现并调用这些工具。
但高可用架构需要比MCP走得更远。直接让Agent调用Java MCP Server有两个问题:第一,Java服务不可用时Agent会阻塞;第二,缺少Python侧的缓存和降级。
参考enterprise-ai-copilot的设计,Python侧应该维护一个只读Tool的本地缓存层:
python
# java_tool_client.py
import asyncio
from typing import Optional
import httpx
class JavaToolClient:
"""Python到Java业务Tool的客户端,带缓存与降级"""
def __init__(self, base_url: str, internal_token: str,
cache_ttl: int = 60):
self.base_url = base_url
self.token = internal_token
self.cache_ttl = cache_ttl
self._cache: dict[str, tuple[float, any]] = {}
async def query_leave_balance(self, employee_id: str) -> dict:
"""查询年假余额(只读,带缓存)"""
cache_key = f"leave_balance:{employee_id}"
cached = self._get_cached(cache_key)
if cached:
return cached
try:
result = await self._call_java_api(
"/internal/leave/balance",
{"employee_id": employee_id}
)
self._set_cache(cache_key, result)
return result
except Exception as e:
# 降级:返回缓存中的过期数据(如有),或明确的错误信息
stale = self._get_cached(cache_key, ignore_ttl=True)
if stale:
return {**stale, "_stale": True}
raise RuntimeError(f"业务系统暂时不可用: {e}")
async def _call_java_api(self, path: str, params: dict) -> dict:
async with httpx.AsyncClient(timeout=5.0) as client:
resp = await client.post(
f"{self.base_url}{path}",
json=params,
headers={"Authorization": f"Bearer {self.token}"}
)
resp.raise_for_status()
return resp.json()
这个设计实现了优雅降级 :Java服务短暂不可用时,返回缓存中的过期数据并标记_stale,让Agent可以决定是继续等待还是告知用户"数据可能不是最新的"。
五、双栈协同的关键工程细节
Runtime Context的可信传递 。在双栈架构中,trace_id、employee_id等字段必须由Java在入口处生成并透传,不能由LLM的tool_call arguments提供。这既是安全要求(防止Prompt注入伪造身份),也是审计要求。
工具可见性的动态收缩 。Planner只有规划权,没有执行授权。allow_business_actions为false时,Proposal工具应该从工具列表中移除,而不是靠LLM"自觉地不调用"。这需要在ToolRegistry层做过滤。
Tool Calling的循环边界 。Spring AI 2.0将工具执行从ChatModel中移出,由ToolCallingAdvisor在外部控制。Python侧的ReAct循环也需要明确的迭代上限和工具调用次数上限,防止Agent陷入"检索-发现不足-再检索"的死循环。
MCP工具的名称空间隔离 。当同时连接多个Java MCP Server时,如果它们暴露了同名工具,需要前缀机制避免冲突。Python的ToolRegistry应该用{server_name}__{tool_name}作为唯一标识。
结语
Python与Java的双栈Agent框架,本质上是一种关注点分离的工程哲学:Python拥有AI编排的灵活性和生态优势,Java拥有企业级系统的可靠性、事务性和安全审计能力。
从代码层面看,核心不是"写两套语言",而是用统一的Tool Registry抽象屏蔽语言边界。Agent层只关心工具的名称、Schema和语义,不关心它是Python函数还是远程Java MCP服务。
如果你正在设计或重构企业级Agent系统,建议从这三个步骤入手:先定义ToolRegistry的接口规范 ,这是双栈架构的"宪法";再实现Java侧的MCP Tool暴露 ,让业务能力标准化输出;最后在Python侧完成ReAct循环与RAG集成,把编排逻辑跑通。这套架构的投入成本不低,但它换来的是:当业务增长十倍时,你不需要重写核心逻辑,只需要在ToolRegistry中注册新的能力。