【Java/Go后端手撸原生Agent(第四篇):原生Function Calling协议接入 + Memory Role修正 + LLM-as-Judge回答完整性校验】
“我”:Java/Go后端开发者、有点时间想自己琢磨,想入门Agent但不想堆砌框架、希望理解底层原理的研发
上一篇链接:Java/Go后端手撸原生Agent(第三篇)
前言

前三篇文章我们从零搭建了ReAct智能体,完成了Pydantic结构化JSON输出、工具Schema自动生成、三态状态机驱动循环、文件读取工具扩展、重复调用代码层拦截等能力。但跑多工具多问题场景时暴露了三个架构级问题:
- 工具调用靠JSON Mode+Prompt约束:我们让LLM输出JSON字符串,自己解析
action/params字段,工具描述塞在System Prompt里用自然语言教模型格式——这本质是用文本约束模拟工具调用协议,模型经常输出格式错误、参数名编造,稳定性差; - 工具结果污染System消息:
add_observation用role="system"存储工具返回值,system消息既承载全局规则又承载动态数据,随着轮次增长原始规则被"推远",LLM注意力稀释忘记规则; - FINISH转移无门禁:模型输出无tool_calls的content就直接FINISH,多问题场景下经常只回答一半就"交卷",靠Prompt写"请完整回答"完全不可靠。
本文完成三大架构升级,是本系列至今认知密度最高的一篇:
- 切换到OpenAI原生Function Calling协议:API层面传tools Schema,响应解析message.tool_calls数组,告别Prompt里教格式、告别脆弱的JSON字符串解析;
- Memory Role语义修正:新增
add_tool_result(tool_call_id, content)用role=tool存工具结果,tool_call_id精确配对调用和结果,assistant消息携带tool_calls数组,system消息不再存入memory; - LLM-as-Judge FINISH Guard:最终回答经一次不带tools的LLM裁判校验完整性,不通过则注入
[Judge反馈]纠偏消息回退重答,校验通过后才正式落盘到memory。
前置说明
- 改造文件:
llm_client.py(适配原生Function Calling请求/响应)、agent/memory.py(role修正+tool_call_id支持)、main.py(四态状态机+Judge门禁); - 删除文件:
agent/schema.py中的ToolAction/FinishResponse/StructuredParser——原生FC下不再需要自定义JSON解析,模型直接返回结构化tool_calls; - 工具层(
tools/base_tool.py、tools/calculator.py、tools/file_reader.py)零改动,Phase 2预留的to_openai_tool_schema()正好被原生FC用上。
一、问题复现与根因分析
1.1 痛点1:JSON Mode模拟工具调用,稳定性差
上一版的做法:System Prompt里用一大段自然语言教模型"两种输出格式严格二选一",模型返回JSON字符串,StructuredParser用json.loads+字典键判断解析。
问题:
- 格式漂移:模型偶尔在JSON外加markdown代码块、加注释文字,解析直接失败;
- 参数幻觉:模型编造不在schema里的参数名(比如给calculator加个
method字段); - 工具描述靠Prompt:工具列表、参数说明全写在System Prompt里自然语言描述,模型"看懂了但不一定按格式来"。
根因:我们在"请求"LLM按指定格式输出,但OpenAI API提供了原生Function Calling能力——你把Schema传给tools参数,模型是在微调阶段就学会了怎么输出结构化的tool_call,这是协议层约束,不是文本层约束。
1.2 痛点2:工具结果role=system,消息语义混乱
上一版:
def add_observation(self, obs: str):
self.history.append({"role": "system", "content": f"[{tool_name}] 返回结果:{obs}"})
问题:system role在OpenAI协议中的语义是"不可变的全局指令",放在messages最前面权重最高。把工具执行结果塞进去会导致:
- system消息越来越长,原始规则被工具数据稀释,长上下文下LLM"看不到"最前面的规则;
- 没有tool_call_id,一次对话多个工具调用时结果和调用对不上;
- role语义混乱——system是规则,tool是数据,混在一起模型分不清哪些是必须遵守的、哪些是参考数据。
1.3 痛点3:FINISH无门禁,回答完整性靠模型自觉
上一版状态机:模型输出无tool_calls的content → 直接FINISH返回。多问题场景(“读文件+解释方法+计算行数”)模型经常解释完方法忘了计算,或者算出数忘了解释——Prompt写"请完整回答所有问题"是规劝不是保证。
根因:和参数校验、重复调用检测一样的问题——把正确性托付给LLM自觉,没有代码层门禁。
二、改造1:LLM客户端适配原生Function Calling
2.1 设计思路
OpenAI Chat Completions API的原生工具调用流程:
- 请求时传
tools参数(工具Schema数组,格式由API定义),不传response_format: {"type":"json_object"}; - 模型如果要调用工具,响应里
message.tool_calls是一个数组,每个元素有id(唯一调用ID)、function.name(工具名)、function.arguments(JSON字符串格式的参数); - 你执行工具后,必须回传两条消息:
- 原assistant消息(携带tool_calls数组)
- 一条或多条
role="tool"消息,每条带tool_call_id对应一个调用
- 模型收到tool结果后继续推理,直到输出无tool_calls的content表示完成。
关键认知:tool_call_id是配对的锚点,模型靠它精确知道"这条工具结果是回应我哪个调用",多工具并行时靠id区分。
2.2 重写llm_client.py
"""
LLM调用客户端(Phase 4:适配原生Function Calling协议)
核心变化:
1. 返回值从str变为LLMResponse对象,同时承载content文本和tool_calls数组;
2. 支持tools参数传入OpenAI Function Schema;
3. 响应解析从只取message.content扩展到同时解析message.tool_calls;
4. ToolCall模型封装id/name/arguments,提供to_openai_dict()用于回传messages。
"""
from __future__ import annotations
import json
import os
from typing import Any, Optional
from dotenv import load_dotenv
from openai import OpenAI
from pydantic import BaseModel, Field
load_dotenv()
class ToolCall(BaseModel):
"""一个工具调用,对应OpenAI message.tool_calls[i]"""
id: str = Field(description="工具调用唯一ID,由API生成,tool结果必须带这个id回传")
name: str = Field(description="要调用的工具名")
arguments: dict[str, Any] = Field(description="工具参数字典(已从JSON字符串解析为dict)")
def to_openai_dict(self) -> dict:
"""序列化为OpenAI messages数组中的tool_calls元素格式"""
return {
"id": self.id,
"type": "function",
"function": {
"name": self.name,
"arguments": json.dumps(self.arguments, ensure_ascii=False),
},
}
class LLMResponse(BaseModel):
"""LLM响应统一封装"""
content: Optional[str] = Field(default=None, description="文本内容(直接回答或推理过程)")
tool_calls: Optional[list[ToolCall]] = Field(default=None, description="工具调用列表,None表示无工具调用")
@property
def has_tool_calls(self) -> bool:
return self.tool_calls is not None and len(self.tool_calls) > 0
def chat_completion(
messages: list[dict],
tools: Optional[list[dict]] = None,
json_mode: bool = False,
) -> LLMResponse:
"""
调用LLM,返回LLMResponse。
- messages: OpenAI格式消息数组
- tools: OpenAI Function Schema数组,传了就走工具调用模式
- json_mode: True时强制JSON输出(Judge校验用),tools模式下不要设为True
"""
client = OpenAI(
api_key=os.getenv("LLM_API_KEY"),
base_url=os.getenv("LLM_BASE_URL"),
)
kwargs: dict[str, Any] = {
"model": os.getenv("LLM_MODEL_NAME"),
"messages": messages,
"temperature": 0.2,
}
if tools is not None:
kwargs["tools"] = tools
kwargs["tool_choice"] = "auto"
if json_mode:
kwargs["response_format"] = {"type": "json_object"}
resp = client.chat.completions.create(**kwargs)
msg = resp.choices[0].message
result = LLMResponse(content=msg.content)
if msg.tool_calls:
parsed_calls = []
for tc in msg.tool_calls:
try:
args_dict = json.loads(tc.function.arguments)
except json.JSONDecodeError:
args_dict = {}
parsed_calls.append(ToolCall(
id=tc.id,
name=tc.function.name,
arguments=args_dict,
))
result.tool_calls = parsed_calls
return result
关键设计点:
ToolCall.id:API生成的唯一标识,后续存tool结果时必须带回这个id,这是配对的关键;to_openai_dict():assistant消息存入memory时需要序列化回OpenAI格式,tool_calls数组里的arguments是字符串不是dict(API协议要求);has_tool_calls属性:主循环判断分支用,替代之前的isinstance(parse_res, ToolAction);tools和json_mode不同时使用:Function Calling模式下不传response_format,Judge校验时不传tools但传json_mode,两种模式互斥。
三、改造2:Memory Role语义修正
3.1 设计思路
对齐OpenAI消息协议,四种role严格分离:
| Role | 用途 | 存入Memory? |
|---|---|---|
| system | 全局指令(SYSTEM_PROMPT) | ❌ 每轮THINKING单独拼接,始终在messages最前端 |
| user | 用户输入+框架注入的前缀标记反馈([Judge反馈]/[系统提示]) |
✅ |
| assistant | LLM输出,可携带tool_calls数组 | ✅ |
| tool | 工具执行结果,带tool_call_id | ✅ |
核心认知:system不存入memory。之前把system连同工具结果一起append导致规则被稀释,现在system每轮独立拼在第一条,位置权重最高。
3.2 重写agent/memory.py
"""
短期对话记忆模块(Phase 5:role语义修正)
消息role严格对齐OpenAI Chat Completions API协议:
- system: 不存入memory(每轮THINKING单独拼接在最前)
- user: 用户输入,以及带前缀标记的框架反馈([Judge反馈]、[系统提示])
- assistant: LLM输出,可携带tool_calls数组
- tool: 工具执行结果,带tool_call_id精确关联调用
"""
from __future__ import annotations
from typing import Any, Optional
class ShortMemory:
"""短期对话记忆,消息格式直接对齐OpenAI API"""
def __init__(self):
self.history: list[dict[str, Any]] = []
def add_user(self, content: str):
"""添加user角色消息"""
self.history.append({"role": "user", "content": content})
def add_assistant(self, content: Optional[str], tool_calls: Optional[list[dict]] = None):
"""
添加assistant消息。
- 直接回答:content=回答文本, tool_calls=None
- 工具调用:content=思考文本(可为None), tool_calls=OpenAI格式工具调用数组
"""
msg: dict[str, Any] = {"role": "assistant", "content": content}
if tool_calls:
msg["tool_calls"] = tool_calls
self.history.append(msg)
def add_tool_result(self, tool_call_id: str, content: str):
"""
添加工具执行结果(role=tool + tool_call_id)。
tool_call_id必须与assistant消息中某条tool_call的id完全一致,
LLM靠id精确配对"哪个结果对应哪个调用"。
"""
self.history.append({
"role": "tool",
"tool_call_id": tool_call_id,
"content": content,
})
def get_messages(self) -> list[dict]:
"""返回全部历史消息(浅拷贝),可直接拼入API请求"""
return [m.copy() for m in self.history]
def remove_last(self) -> Optional[dict]:
"""移除最后一条消息并返回,用于撤销未通过校验的草稿"""
if self.history:
return self.history.pop()
return None
和上一版的对比:
- 删除了
add_observation(role=system存工具结果)→ 新增add_tool_result(tool_call_id, content)用role=tool; add_assistant新增可选tool_calls参数,工具调用场景下把OpenAI格式的tool_calls数组一并存入;- system消息不再存入history,
get_messages()只返回user/assistant/tool三种消息; remove_last()提供撤销能力,FINISH Guard校验不通过时可以撤销未通过的草稿(虽然最终版本没用这个,改为"校验通过后才落盘"的策略)。
四、改造3:四态状态机+LLM-as-Judge FINISH Guard
4.1 为什么是四态而不是三态
上一版状态机:THINKING / TOOL_EXECUTING / FINISHED。问题是"模型输出final answer"直接转移到FINISHED,中间没有校验环节。新增VALIDATING状态:
THINKING ──(has tool_calls)──→ TOOL_EXECUTING ──(执行完)──→ THINKING
│ ↑
│(no tool_calls, 草稿回答) │
↓ │
VALIDATING ──(Judge不通过, 注入纠偏)────────────────────────→┘
│
│(Judge通过)
↓
FINISHED
VALIDATING的本质:在THINKING→FINISHED的转移路径上加一个门禁状态,不通过就回退。这和参数校验在工具调用前做校验、重复调用检测在工具执行前做拦截是同一套设计哲学。
4.2 LLM-as-Judge为什么比规则门禁好
最初我用硬编码规则做门禁:
if "calculator" in invoked_tools and not has_number(answer): # 拦截
if "read_file" in invoked_tools and len(answer) < 40: # 拦截
问题很明显:每加一个工具就要加规则,不可扩展。LLM-as-Judge用一次不带tools的LLM调用做裁判,输入(用户问题, AI回答),输出结构化判断{passed: bool, missing: str}:
- 通用性:不需要知道有哪些工具,直接理解语义差距;
- 精确反馈:不只是"不通过",还指出具体缺什么(“缺少对execute方法的解释”),执行LLM拿到精确反馈补答;
- 成本低:Judge用json_mode,输入短输出固定,通常<200 input tokens + <50 output tokens。
4.3 Final answer先校验后落盘
一个关键细节:之前assistant消息在THINKING分支里不管什么情况都立刻写入memory。但如果模型输出的是不合格的final answer(被Judge打回),这个草稿不应该留在对话历史里——否则模型"看到自己已经回答了"就不会认真补答。
策略:
- tool_calls的assistant消息:立刻写入memory(因为tool结果必须紧跟在assistant消息后面配对);
- final answer的assistant消息:不立即写入,存到
pending_draft_answer,VALIDATING通过后才add_assistant正式落盘。
4.4 完整main.py
"""
Phase 4+5+6:原生Function Calling + Memory Role修正 + LLM-as-Judge FINISH Guard
"""
from __future__ import annotations
import json
from enum import Enum
from typing import Optional
from pydantic import BaseModel, Field
from agent.memory import ShortMemory
from llm_client import ToolCall, chat_completion
from tools.base_tool import BaseTool
from tools.calculator import CalcTool
from tools.file_reader import FileReadTool
class AgentTaskState(Enum):
THINKING = "thinking"
TOOL_EXECUTING = "tool_executing"
VALIDATING = "validating"
FINISHED = "finished"
tool_list: list[BaseTool] = [CalcTool(), FileReadTool()]
tool_map = {t.name: t for t in tool_list}
SYSTEM_PROMPT = """你是一个智能代码助手,可以使用提供的工具来帮助用户完成任务。
规则:
1. 需要获取外部信息(读取文件、计算数值等)时,调用相应工具;
2. 仔细阅读工具返回结果,基于结果进行推理;
3. 信息足够回答用户问题时,直接用清晰的自然语言回答,不要调用不必要的工具;
4. 如果用户要求数值计算,必须使用calculator工具,不要心算;
5. 如果工具返回错误信息,分析错误原因,可以换参数重试或告知用户;
6. 回答要结构清晰、条理分明、完整覆盖用户的所有问题点,使用中文。
"""
# ========== LLM-as-Judge 完整性校验 ==========
class JudgeResult(BaseModel):
"""Judge的结构化输出,json_mode强约束"""
passed: bool = Field(description="回答是否完整覆盖了用户的所有问题点")
missing: str = Field(description="不通过时具体缺少什么;通过时为空字符串")
JUDGE_SYSTEM_PROMPT = """你是回答质量检查员。你的唯一任务是判断"AI助手的回答"是否完整覆盖了"用户的问题"中的所有要求。
判断标准:
- 用户问了几个问题/任务点,回答里每个都要有对应内容;
- 如果用户要求"必须调用工具计算",回答必须包含工具算出的具体数值结果;
- 如果用户要求"读取并解释代码/文件",回答必须包含对代码的实质解释,不能只是说"已读取";
- 回答允许有推理过程,但必须最终给出答案。
输出严格JSON格式:
{"passed": true/false, "missing": "具体缺失内容描述,passed为true时为空字符串"}
注意:
- 你只做检查,不回答用户问题本身;
- missing字段要具体指出缺什么(例如"缺少对execute方法的解释"、"没有给出计算结果"),不要泛泛说"回答不完整";
- 如果回答覆盖了所有问题点,passed=true,missing为空。
"""
def judge_answer(user_query: str, answer: str) -> JudgeResult:
"""用LLM裁判判断回答完整性"""
# 兜底:极短回答直接判不通过,省一次LLM调用
if not answer or len(answer.strip()) < 10:
return JudgeResult(passed=False, missing="回答过短,没有实质内容。")
messages = [
{"role": "system", "content": JUDGE_SYSTEM_PROMPT},
{"role": "user", "content": f"【用户问题】{user_query}\n\n【AI回答】{answer}\n\n请判断AI回答是否完整覆盖了用户的所有问题点。"},
]
resp = chat_completion(messages, json_mode=True)
if not resp.content:
return JudgeResult(passed=True, missing="") # Judge异常时保守通过(fail-open)
try:
data = json.loads(resp.content)
return JudgeResult(**data)
except Exception:
return JudgeResult(passed=True, missing="")
def run_agent(user_query: str):
memory = ShortMemory()
memory.add_user(user_query)
max_loop = 15
state = AgentTaskState.THINKING
loop_count = 0
judge_retry_count = 0
max_judge_retries = 3
final_answer = None
pending_tool_call: Optional[ToolCall] = None
pending_draft_answer: Optional[str] = None # 待校验草稿,通过后才落盘
executed_calls: set[tuple[str, str]] = set()
tools_schema = [t.to_openai_tool_schema() for t in tool_list]
while state != AgentTaskState.FINISHED and loop_count < max_loop:
loop_count += 1
if state == AgentTaskState.THINKING:
# system每轮独立拼接在最前,不存入memory
messages = [{"role": "system", "content": SYSTEM_PROMPT}]
messages.extend(memory.get_messages())
print(f"\n=== 第{loop_count}轮 THINKING ===")
resp = chat_completion(messages, tools=tools_schema)
if resp.has_tool_calls:
# 工具调用:assistant消息立刻写入(tool结果必须紧跟其后配对)
tool_calls_dicts = [tc.to_openai_dict() for tc in resp.tool_calls]
memory.add_assistant(content=resp.content, tool_calls=tool_calls_dicts)
if resp.content:
thought = resp.content[:200] + ("..." if len(resp.content) > 200 else "")
print(f"【推理思考】{thought}")
pending_tool_call = resp.tool_calls[0] # 简化:每次处理一个工具调用
state = AgentTaskState.TOOL_EXECUTING
continue
else:
# 无tool_calls:模型尝试直接回答,先存为草稿,校验通过后才落盘
pending_draft_answer = resp.content
state = AgentTaskState.VALIDATING
continue
if state == AgentTaskState.TOOL_EXECUTING:
call_key = (
pending_tool_call.name,
json.dumps(pending_tool_call.arguments, sort_keys=True, ensure_ascii=False),
)
if call_key in executed_calls:
obs = (
f"[系统提示] 你已经用完全相同的参数调用过{pending_tool_call.name},"
f"结果已在上方消息中。请直接基于已有信息回答,不要重复调用。"
)
print(f"【重复调用拦截】{pending_tool_call.name} {pending_tool_call.arguments}")
else:
print(f"【工具调用】{pending_tool_call.name}("
f"{json.dumps(pending_tool_call.arguments, ensure_ascii=False)})")
tool = tool_map.get(pending_tool_call.name)
if not tool:
obs = f"错误:不存在工具{pending_tool_call.name}"
else:
obs = tool.execute(pending_tool_call.arguments)
display_obs = obs[:300] + ("..." if len(obs) > 300 else "")
print(f"【工具返回】{display_obs}")
executed_calls.add(call_key)
# 工具结果通过role=tool + tool_call_id存入memory
memory.add_tool_result(pending_tool_call.id, obs)
pending_tool_call = None
state = AgentTaskState.THINKING
continue
if state == AgentTaskState.VALIDATING:
print("【校验】正在Judge回答完整性...")
judge_res = judge_answer(user_query, pending_draft_answer)
if judge_res.passed or judge_retry_count >= max_judge_retries:
if judge_retry_count >= max_judge_retries and not judge_res.passed:
print(f"【警告】Judge已连续{max_judge_retries}次打回,强制通过防止死循环")
else:
print("【校验通过】回答完整")
# 校验通过:此时才把final answer正式写入memory
memory.add_assistant(content=pending_draft_answer)
final_answer = pending_draft_answer
state = AgentTaskState.FINISHED
break
else:
judge_retry_count += 1
print(f"【门禁拦截】缺失: {judge_res.missing}")
# 纠偏消息用role=user,带[Judge反馈]前缀标记
correction = (
f"你的回答不完整,缺少以下内容:{judge_res.missing}\n"
f"请基于已有工具结果补充回答,不要重复调用已经调用过的工具。"
f"如果信息不足,可以调用尚未使用过的工具。"
)
memory.add_user(f"[Judge反馈] {correction}")
pending_draft_answer = None
state = AgentTaskState.THINKING
continue
if final_answer is None:
final_answer = f"达到最大循环次数{max_loop},任务未完成"
return final_answer
if __name__ == "__main__":
answer = run_agent(
"帮我读一下 tools/base_tool.py 的内容,"
"然后解释execute和to_openai_tool_schema方法做了什么,"
"这两个方法的行数加起来乘以4再除以2等于多少(必须调用工具计算)"
)
print("\n最终回答:", answer)
五、运行效果
多工具多问题场景完整链路(读文件+解释+计算):
=== 第1轮 THINKING ===
【推理思考】用户要求读取文件、解释方法、计算数值。首先读取文件。
【工具调用】read_file({"path": "tools/base_tool.py"})
【工具返回】[文件: .../tools/base_tool.py] 共56行...
1 | from abc import ABC...
28 | def execute(self, params: dict) -> str:
...
=== 第2轮 THINKING ===
【推理思考】文件已读取,execute是28-33行(6行),to_openai_tool_schema是35-43行(9行)。
需要调用计算器计算 (6+9)*4/2。
【工具调用】calculator({"expr": "(6+9)*4/2"})
【工具返回】计算结果: (6+9)*4/2 = 30.0
=== 第3轮 THINKING ===
【校验】正在Judge回答完整性...
【校验通过】回答完整
最终回答: 已读取文件内容,两个方法说明如下:
- **execute方法**(第28-33行,共6行):BaseTool的模板方法...
- **to_openai_tool_schema方法**(第35-43行,共9行):生成OpenAI标准Function Call Schema...
两个方法共15行,乘以4再除以2等于30。
共3轮,无重复调用、无死循环、无遗漏子问题。
六、核心改造总结
| 维度 | Phase 3(改造前) | Phase 4+5+6(改造后) |
|---|---|---|
| 工具调用方式 | JSON Mode+Prompt自然语言约束+手动JSON解析 | 原生Function Calling协议(tools参数+tool_calls响应) |
| 工具结果role | role=system(污染全局指令) | role=tool + tool_call_id(精确配对) |
| system消息管理 | 存入memory随轮次增长 | 每轮独立拼接,始终在最前端 |
| assistant消息格式 | {"role":"assistant","content":"..."}纯文本 |
可携带tool_calls数组,对齐API协议 |
| FINISH转移门禁 | 无(无tool_calls即结束) | VALIDATING状态+LLM-as-Judge校验 |
| 失败重试机制 | 无 | Judge不通过注入[Judge反馈]精确纠偏,最多3次 |
| 回答落盘时机 | 生成即写入memory | Judge通过后才写入(草稿不落盘) |
| 添加新工具 | 要在Prompt里手写工具描述 | 工具类定义→注册到tool_list,零侵入 |
| 重复调用防护 | executed_calls set | 保留,拦截提示通过tool_result content传递 |
七、后续拓展
- 多工具并行调用:当前简化为每次只处理
resp.tool_calls[0]一个调用,可扩展为遍历tool_calls数组并发执行多个工具,结果分别用对应tool_call_id存入; - BashTool/FileWriteTool:扩展工具集为真正可用的Coding Agent(注意危险命令白名单、输出截断、权限沙箱);
- Plan-then-Execute模式:复杂任务先让LLM生成子任务计划,执行中跟踪进度,完成全部子任务才允许FINISH;
- 长期记忆/RAG:接入Chroma向量库,解决上下文窗口限制和跨会话知识积累;
- Hook/Callback机制:抽离AgentEngine类,支持before_llm/after_tool/on_state_change等事件钩子,方便加日志、trace、指标。
八、Java/Go后端快速语法映射
ToolCall(BaseModel)= Java POJO / Go struct,带ID和参数字段Optional[str]= JavaOptional<String>/ Go*string(指针可空)@property抽象属性 = 接口getter方法- 四态状态机while+continue = State Pattern / 状态机DSL
- LLM-as-Judge = AOP切面校验 / 中间件模式(请求经过校验层才放行)
tool_call_id配对 = 分布式链路traceId / 消息队列correlation_id- 模板方法
execute(基类校验→子类run) = Servlet Filter / Interceptor模式 - fail-open保守策略 = 熔断器降级(校验器异常时放行而非阻塞主流程)
更多推荐


所有评论(0)