Java/Go后端手撸原生Agent(第二篇):Pydantic结构化输出改造,工程化解决工具死循环问题
上篇链接Java/Go后端手撸原生Agent”我“Java/Go后端开发者、有点时间想自己琢磨想入门Agent但不想堆砌框架、希望理解底层原理的研发正文前言上篇文章我们基于纯Python原生代码不依赖LangChain/LangGraph从零搭建了一套基础ReAct智能体实现了工具抽象、短期记忆、裸HTTP调用大模型。上篇完整工程环境配置、LLM客户端、工具基类、文本解析ReAct、主循环调度。本文作为续集完成两大核心工程级升级抛弃脆弱的文本分割解析使用Pydantic实现结构化JSON输出对标厂商标准Function Call修复原生Agent高频Bug工具重复调用死循环采用状态机工程化方案。前置说明完全复用上篇所有基础文件env_loader.py、memory.py、tools/工具包、llm_client.py基础版本新增文件agent/schema.pyPydantic实体、agent/structured_parser.py结构化解析器删除旧文本解析器parser.py核心改造main.py主循环、消息组装规范、死循环防护逻辑。一、拓展1Pydantic结构化输出改造替代文本ReAct解析1.1 改造思路后端视角文本分割解析硬伤LLM输出换行、注释、markdown会直接解析崩溃容错极低Pydantic等价Java POJO / Go Struct强类型校验、自动序列化/反序列化开启模型response_formatjson_object强制返回标准JSON对齐OpenAI/通义/DeepSeek原生Function Call协议注部分模型可能不支持该字段双分支实体工具调用动作、任务完成最终回答二选一输出。1.2 新建结构化模型 agent/schema.pyfrompydanticimportBaseModel,FieldfromtypingimportDict,Union# 场景1需要调用工具classToolAction(BaseModel):thought:strField(description推理思考过程)action:strField(description工具名称无工具固定填 FINISH)params:DictField(description工具入参json对象无参数传空{})# 场景2任务结束直接返回最终答案classFinishResponse(BaseModel):final_answer:strField(description无需调用工具整理后给用户的最终回答)# 联合类型LLM输出二选一AgentOutputToolAction|FinishResponse1.3 改造LLM客户端 llm_client.py支持JSON强输出新增json_mode参数控制结构化输出开关底层透传response_formatfrompydanticimportBaseModelfromenv_loaderimportLLMConfigimportrequestsclassResponseFormat(BaseModel):type:strclassChatRequest(BaseModel):model:strmessages:list[dict[str,str]]temperature:floatresponse_format:ResponseFormat|NoneNonedefchat_completion(messages:list[dict[str,str]],json_mode:boolFalse# 是否开启结构化输出)-str:headers{Authorization:fBearer{LLMConfig.API_KEY},Content-Type:application/json}reqChatRequest(modelLLMConfig.MODEL_NAME,messagesmessages,temperature0.1)# 开启json强制输出ifjson_mode:req.response_formatResponseFormat(typejson_object)bodyreq.model_dump()resprequests.post(f{LLMConfig.BASE_URL}/chat/completions,headersheaders,jsonbody)resp.raise_for_status()returnresp.json()[choices][0][message][content]#测试入口if__name____main__:reschat_completion([{role:user,content:你好}],json_modeTrue)print(res)1.4 结构化解析器 agent/structured_parser.py替代旧文本解析器自动JSON反序列化为Pydantic实体自带参数校验importjsonfromagent.schemaimportAgentOutput,ToolAction,FinishResponseclassStructuredParser:staticmethoddefparse_json(raw_json_str:str)-AgentOutput|None: 解析大模型返回的JSON字符串自动区分 ToolAction / FinishResponse try:datajson.loads(raw_json_str)ifactionindata:returnToolAction(**data)eliffinal_answerindata:returnFinishResponse(**data)else:returnNoneexceptExceptionase:print(f结构化解析失败{str(e)}原始输出{raw_json_str})returnNone1.5 规范消息组装角色隔离修复字符串拼接Bug上篇存在致命不规范写法将system规则全部历史对话拼接为一段字符串塞进单条system消息角色边界丢失模型极易混淆user/工具观测数据。工程标准写法第一条消息固定全局System规则记忆内每条对话独立追加区分user/assistant/system(工具观测)# 标准消息组装代码messages[{role:system,content:SYSTEM_PROMPT}]# 第二步追加所有历史对话、工具观测记录每条独立role隔离messages.extend(memory.get_raw_dict_list())同步微调短期记忆agent/memory.py保证输出标准OpenAI消息结构fromtypingimportTypedDictclassMessage(TypedDict):role:strcontent:strclassShortMemory:def__init__(self):self.history:list[Message][]defadd_user(self,content:str):用户的输入self.history.append({role:user,content:content})defadd_assistant(self,content:str):self.history.append({role:assistant,content:content})defadd_observation(self,content:str):工具返回的观察结果self.history.append({role:system,content:content})defget_all(self)-list[Message]:returnself.history.copy()defget_raw_dict_list(self)-list[dict[str,str]]:# 直接返回原生dict数组适配LLM入参return[{role:m[role],content:m[content]}forminself.history]1.6 重写系统提示词约束模型输出行为不再混入对话上下文仅定义全局规则、工具、输出格式上下文完全交给messages数组管理SYSTEM_PROMPT 你是支持工具调用的智能助手必须仅输出纯JSON禁止额外文字、Markdown、换行注释。 可用工具 calculator数学计算器参数expr为数学表达式例{expr:(10020)*5} 严格遵守执行规则 1. 首次缺少数值时调用calculator获取计算结果 2. 一旦收到system角色的工具观测结果已算出数字**禁止再次调用任何工具**必须直接输出final_answer总结答案 3. 两种输出格式严格二选一 - 需要调用工具时{thought:推理过程,action:工具名称,params:{expr:表达式}} - 已有工具计算结果、无需工具{final_answer:把计算结果整理成自然语言回答用户} 二、线上运行Bug复现工具无限重复调用死循环2.1 问题现象执行计算(100 20) * 5模型第一次调用计算器拿到结果后无视提示词约束持续重复调用同一工具耗尽最大循环次数才终止不会输出final_answer。完整日志特征第一轮用户提问 → 调用计算器 → 存入system观测记录第二轮上下文携带观测结果模型偶尔输出final_answer但代码未立刻return循环继续后续轮次上下文叠加历史assistant回答模型逻辑混乱反复执行工具直到max_loop5结束。2.2 根因工程层面非单纯提示词问题核心逻辑漏洞解析到FinishResponse仅写入记忆无return终止循环无显式任务状态标记全靠模型输出控制流程模型不稳定直接失效缺少多层前置拦截防护仅靠提示词约束容错率极低未检测记忆中已存在最终回答仍继续执行LLM推理。三、工程化根治方案状态机3.1 设计思路后端标准状态机思想新增任务状态枚举显式区分运行/完成状态驱动流程命中FinishResponse立即return终止整个函数循环耗尽兜底逻辑读取已生成的历史回答避免无结果返回可不做真实遇到这样说明我们的处理流程还是有问题。3.2 完整重写 run_agent 主循环 main.pyfromagent.memoryimportShortMemoryfromenumimportEnumfromagent.schemaimportFinishResponse,ToolActionfromagent.structured_parserimportStructuredParserfromllm_clientimportchat_completionfromtools.base_toolimportBaseToolfromtools.calculatorimportCalcTool# 1. 新增状态枚举工程化状态机替代隐式类型判断classAgentTaskState(Enum):RUNNINGrunningFINISHEDfinished# 注册所有可用工具tool_list:list[BaseTool][CalcTool()]tool_map{t.name:tfortintool_list}# 系统提示词规定ReAct输出格式SYSTEM_PROMPT 你是支持工具调用的智能助手必须仅输出纯JSON禁止额外文字、Markdown、换行注释。 可用工具 calculator数学计算器参数expr为数学表达式例{expr:(10020)*5} 严格遵守执行规则 1. 首次缺少数值时调用calculator获取计算结果 2. 一旦收到system角色的工具观测结果已算出数字**禁止再次调用任何工具**必须直接输出final_answer总结答案 3. 两种输出格式严格二选一 - 需要调用工具时{thought:推理过程,action:工具名称,params:{expr:表达式}} - 已有工具计算结果、无需工具{final_answer:把计算结果整理成自然语言回答用户} defrun_agent(user_query:str):memoryShortMemory()memory.add_user(user_query)max_loop5foriinrange(max_loop):# 标准规范消息组装固定system在前记忆消息在后 # 第一步根系统提示词全局规则、工具定义、输出JSON约束独立system消息messages[{role:system,content:SYSTEM_PROMPT}]# 第二步追加所有历史对话、工具观测记录每条独立role隔离messages.extend(memory.get_raw_dict_list())# 调试打印完整上下文print( 当前完整上下文 )formsginmessages:print(msg)# 调用LLM开启结构化JSON输出llm_raw_jsonchat_completion(messages,json_modeTrue)parse_resStructuredParser.parse_json(llm_raw_json)ifparse_resisNone:returnf模型输出格式解析失败原始内容{llm_raw_json}# 分支1任务完成直接返回最终答案ifisinstance(parse_res,FinishResponse):memory.add_assistant(parse_res.final_answer)task_stateAgentTaskState.FINISHEDreturnparse_res.final_answer# 分支2执行工具调用流程ifisinstance(parse_res,ToolAction):tool_nameparse_res.action tool_paramsparse_res.paramsprint(f【推理思考】{parse_res.thought})print(f【工具调用】name{tool_name}, params{tool_params})tooltool_map.get(tool_name)ifnottool:obsf异常不存在工具{tool_name}else:obstool.run(tool_params)print(f【工具返回结果】{obs}\n)# 工具观测存入记忆下一轮循环自动拼入messagesmemory.add_observation(obs)returnf达到最大循环次数{max_loop}任务未完成if__name____main__:answerrun_agent(计算 (100 20) * 5)print(最终回答:,answer)四、核心修复点总结修复致命逻辑缺陷命中FinishResponse直接return杜绝循环继续执行消息分层规范独立system全局规则多角色分离历史消息对齐大模型标准对话协议显式状态机管控AgentTaskState枚举统一管理任务流转方便后续扩展超时、中断、分支流程Pydantic结构化替代文本解析消除LLM输出不规则导致的解析崩溃问题。五、修复后预期运行效果 当前完整上下文 {role: system, content: \n你是支持工具调用的智能助手必须仅输出纯JSON禁止额外文字、Markdown、换行注释。\n可用工具\ncalculator数学计算器参数expr为数学表达式例{expr:(10020)*5}\n\n严格遵守执行规则\n1. 首次缺少数值时调用calculator获取计算结果\n2. 一旦收到system角色的工具观测结果已算出数字**禁止再次调用任何工具**必须直接输出final_answer总结答案\n3. 两种输出格式严格二选一\n- 需要调用工具时{thought:推理过程,action:工具名称,params:{expr:表达式}}\n- 已有工具计算结果、无需工具{final_answer:把计算结果整理成自然语言回答用户}\n} {role: user, content: 计算 (100 20) * 5} 【推理思考】用户需要计算数学表达式 (100 20) * 5我需要调用计算器工具来获取结果。 【工具调用】namecalculator, params{expr: (100 20) * 5} 【工具返回结果】计算结果: (100 20) * 5 600 当前完整上下文 {role: system, content: \n你是支持工具调用的智能助手必须仅输出纯JSON禁止额外文字、Markdown、换行注释。\n可用工具\ncalculator数学计算器参数expr为数学表达式例{expr:(10020)*5}\n\n严格遵守执行规则\n1. 首次缺少数值时调用calculator获取计算结果\n2. 一旦收到system角色的工具观测结果已算出数字**禁止再次调用任何工具**必须直接输出final_answer总结答案\n3. 两种输出格式严格二选一\n- 需要调用工具时{thought:推理过程,action:工具名称,params:{expr:表达式}}\n- 已有工具计算结果、无需工具{final_answer:把计算结果整理成自然语言回答用户}\n} {role: user, content: 计算 (100 20) * 5} {role: system, content: 计算结果: (100 20) * 5 600} 最终回答: 计算结果是 600仅执行一轮工具调用直接输出最终回答不会出现多轮重复调用、耗尽循环的问题。六、后续拓展自动基于Pydantic模型生成Function Call标准工具描述Schema对接模型原生工具调用接口新增文件读取工具FileReadTool实现代码读取Agent接入Chroma向量数据库实现长期记忆RAG封装AgentEngine类面向对象工程化重构拆分日志、异常、指标模块。

相关新闻

AutoVLA论文阅读笔记

AutoVLA论文阅读笔记

论文:https://arxiv.org/pdf/2506.13757 代码: 1、为什么要做这个研究(理论走向和目前缺陷) ? 之前的vla模型要么结构复杂,要么梯度断连,自回归式的输出路点虽然优雅,但是llm天然不太适合这种精…

2026/10/28 4:25:54 阅读更多 →
基于大模型的智能留学咨询系统设计与优化

基于大模型的智能留学咨询系统设计与优化

1. 项目概述:留学咨询大模型的智能办理系统这个项目本质上是一个基于大语言模型的智能留学咨询系统,核心目标是通过AI技术实现留学申请流程的自动化处理。不同于传统留学中介的人工服务模式,我们采用大模型作为底层引擎,结合业务规…

2026/10/26 22:48:54 阅读更多 →
项目开发必备:如何系统管理前置条件(Prerequisites)

项目开发必备:如何系统管理前置条件(Prerequisites)

1. 项目概述"0.Prerequisites"这个标题看似简单,却蕴含着项目开发中最容易被忽视的关键环节。作为从业十余年的老手,我见过太多项目因为前期准备不足而中途夭折的案例。这个编号为0的阶段,实际上决定了整个项目的成败基础。在技术领…

2026/10/24 12:22:24 阅读更多 →

最新新闻

Electron桌面应用开发实战:从环境搭建到打包发布全流程

Electron桌面应用开发实战:从环境搭建到打包发布全流程

做前端这些年,我大部分时间都在跟浏览器标签页打交道,直到第一次把桌面应用跑起来,才发现原来自己一直被限制在地址栏和刷新按钮围起来的框框里。Electron这个词,最开始是在某段时间频繁出现在同事的讨论里,一个用HTML…

2026/10/27 17:33:32 阅读更多 →
滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

简介:亚太天能T109滑盖智能锁产品说明书是一份PDF格式的官方技术文档,主要面向智能锁安装人员、终端用户及售后维护者,帮助快速掌握产品安装、配置与常见故障处理。全包仅1个PDF文件,大小4.37MB,内容结构清晰&#xff…

2026/10/27 4:06:49 阅读更多 →
PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

上个月我在调试一台高速AGV的驱动系统,整车要求空载跑到8m/s,电机转速已经拉到额定点,驱动器的输出电压也顶到了母线电压极限附近。再往上踩速度指令,转速纹丝不动,三相电流反而开始震荡。那会儿我的第一反应就是&…

2026/10/27 7:21:53 阅读更多 →
C++核心知识点:从内存管理到STL的实战指南

C++核心知识点:从内存管理到STL的实战指南

C 这门语言我前前后后折腾了十几年,用它写过嵌入式驱动、做过游戏服务端,也搭过基础组件库。每次看到新人一头扎进"C 核心知识点"这个搜索词,我其实挺感慨的——这门语言的难点从来不是语法多,而是知识点之间的耦合太深…

2026/10/27 18:20:50 阅读更多 →
T109滑盖智能锁怎么装?安装调试与故障排查全攻略

T109滑盖智能锁怎么装?安装调试与故障排查全攻略

简介:亚太天能T109滑盖智能锁产品说明书以PDF格式发布,适合智能锁用户、安装人员与售后维修人员阅读。内容围绕T109系列指纹密码刷卡锁展开,先介绍前面板、后面板、锁体、机械钥匙、电池盒盖等部件名称与功能,再给出完整安装步骤&…

2026/10/27 6:20:47 阅读更多 →
LLM知识降熵:从信息垄断到理解权下放

LLM知识降熵:从信息垄断到理解权下放

你有没有过这种经历:翻开源码注释、论文摘要或者一份专业报告,每个字都认识,连在一起却像天书。想找人聊聊,身边要么没人懂,要么懂的懒得跟你解释。后来我试着把同一段内容丢给一个大语言模型,几秒钟之后&a…

2026/10/27 4:06:50 阅读更多 →

日新闻

周新闻

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/27 13:55:19 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/27 7:21:50 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/27 19:19:37 阅读更多 →