
AI Agent开发在2026年已经不只是大模型厂商的专属话题而是后端工程师、算法工程师和运维团队都要面对的实际工程问题。很多人已经不再问“大模型能做什么”而是开始问“怎么让大模型在自己的系统里稳定地调用工具、查数据、做决策、处理异常”。这篇文章围绕“从零基础到企业级项目实战”这条主线把 AI Agent 开发的完整链路讲透先理解 Agent 的核心运行逻辑再搭建可复现的开发环境然后分别用原生代码和主流框架实现一个可运行的 Agent接着补上 Spring Boot 侧客户端集成、运行验证、故障排查和生产化改造。整篇内容都以工程落地为目标不写概念空谈也不堆没有出处的资料结论。如果你之前只调用过单次大模型接口或者只做过 Prompt 工程那么这篇文章会把两者之间的那道“工程鸿沟”补上。学完后你不仅能写一个最小可运行的 Agent 示例还能知道它为什么这么设计、上线前还要处理哪些问题。1. 先理解 AI Agent 的运行逻辑再看代码1.1 Agent 到底是什么用一句话概括AI Agent 是一种“能自主拆解任务、调用外部工具、观察执行结果、调整下一步动作”的大模型应用。它和普通的大模型对话程序最大的区别是它有一个行动循环。传统的大模型调用是“问一句、答一句”。Agent 则更像是“领到一个目标自己拆分子步骤完成一步、检查一步、再决定下一步”。比如用户问“帮我查一下订单状态并解释为什么需要补差价”单次 Prompt 调用可能只能根据上下文给出一个笼统回答而 Agent 可以先调用订单查询工具拿到真实订单数据再根据数据解释差价原因如果数据缺失还能自动追问用户或查另一个系统。技术定义上Agent 不是单一模型而是由以下要素组成的程序大模型负责理解任务、生成规划、决策下一步动作。工具注册表把外部能力数据库查询、HTTP 接口、文件操作、内部 API暴露给模型。记忆短期上下文和工作区状态、长期知识库或外部存储。执行循环模型输出决策程序执行工具把结果再交给模型直到任务完成或达到上限。安全边界限制模型能访问的工具、数据和操作范围。在实际项目中Agent 更像一个“智能班长”它自己做不完所有事但知道去哪里问、找谁做、怎么判断结果靠不靠谱。1.2 Agent 与单次大模型调用的关键区别下面这张表可以快速理解为什么不能只在单次 Prompt 里“硬塞”所有逻辑对比维度单次 Prompt 调用AI Agent任务目标回答当前问题完成一个可能包含多步骤的目标外部数据依赖上下文中已经存在的文本可以主动调用工具获取实时数据工具调用不涉及或只做格式要求需要工具定义、参数解析、结果回填错误处理回答错误后用户重新提问Agent 可以观察工具结果并自动重试或换策略上下文控制一次请求写完超长就截断需要管理多轮消息、工具结果和状态成本控制单次请求消耗可预估多轮循环可能导致成本放大必须设上限工程复杂度较低适合简单问答需要设计循环、退出条件、日志、权限和评估这个区别决定了 Agent 开发的核心难点不是模型有多聪明而是你的工程系统能不能承接模型不断变化的输出。1.3 Agent 的典型工作链路规划、工具、记忆、反思运行一个 Agent 时后台通常会发生这样一条链路理解目标模型读取系统提示词和用户请求明确要完成什么。拆解计划模型决定需要调用哪个工具、传什么参数、先做哪一步。程序执行你的代码而不是模型本身去调用真实工具拿到结构化结果。观察结果工具结果作为新消息回传给模型模型判断结果是否符合预期。继续或结束如果还不够再次调用工具如果已经完成生成最终回复。这里面最容易被忽略的是“记忆”和“反思”。短期记忆指当前会话上下文中的用户输入、工具调用和返回结果。长期记忆指跨会话的用户偏好、历史结论和知识库内容。反思则是指模型在拿到工具结果后不是无条件接受而是判断“这个结果是对的吗够吗需要再查一个数据吗”。有的 Agent 框架把反思做成了显式节点比如“在回答前先验证计算结果是否合理”。在自研 Agent 里这种反思逻辑可以靠 Prompt 引导实现也可以靠代码规则实现。结论是不要把所有判断都交给模型能用代码判断的边界尽量用代码。1.4 什么场景适合用 Agent什么场景不该用Agent 不是银弹。它的价值在于“需要动态编排多个工具”的场景。适合用的场景企业内部知识库问答需要检索文档并生成回答。工单处理需要查询系统状态、调接口修改状态、生成处理记录。数据分析助手需要根据用户自然语言生成查询、执行查询、解释结果。运维诊断助手需要读取日志、检查指标、给出修复建议。客服辅助需要查订单、查政策、计算赔偿金额。不适合用的场景简单固定的翻译、改写、摘要用单次 Prompt 或普通 API 即可。对响应时间要求极高的接口Agent 多轮调度会明显增加延迟。无法接受模型输出不确定性的场景比如财务精确记账应使用确定性代码并在完成后人工确认。工具数量少且调用路径完全固定时用状态机或脚本比 Agent 更可靠。这里有一个重要判断Agent 的价值在动态编排不在“显得聪明”。如果业务流程本身是固定的就别硬套 Agent。2. 环境准备搭一套最小可复现的 AI Agent 开发环境2.1 技术选型先手写再上框架很多人在第一步就纠结“用 LangChain 还是 LangGraph还是 Spring AI”。我的建议是分两步学习阶段先用原生代码手写一个 Agent 循环代码量在 200 行以内。这样你能理解工具调用、消息回填、循环退出这些底层机制。生产阶段再根据团队技术栈选择框架或自研编排引擎。框架能减少重复代码但也带来了版本变动、抽象层级和排障复杂度。选型没有绝对最优解关键看团队技术栈方案适合场景学习成本风险点原生 Python 实现理解原理、快速原型、团队规模小中所有轮子要自己写LangChain / LangGraph快速集成文档、向量库、各类工具中高版本频繁变化API 迁移成本Spring AIJava / Spring Boot 技术栈团队中模块更新快需核对版本自研编排引擎业务链路复杂企业要完全可控高初期研发投入大低代码平台非技术背景快速验证低定制空间有限难以深度集成推荐新手走“原生代码理解逻辑 - LangChain/LangGraph 做进阶 - 回到工程化设计”这条路径。2.2 开发环境清单下面的环境不是官方定死的而是目前最常见的搭配。落地前先确认自己团队的约定版本组件版本建议用途Python3.10 或更高编写 Agent 主逻辑、工具调用脚本Java17 或更高Spring Boot 客户端和中间服务开发Node.js18 或更高可选前端或脚本工具Docker20.10本地启动向量库、数据库等依赖Redis7.x会话状态、缓存、限流向量数据库可选Milvus、Qdrant、pgvector知识库检索大模型服务OpenAI 兼容接口或云厂商模型服务模型接入和推理API Key在环境变量中配置调用模型服务这里不绑定某一家模型服务商。实际项目里你只需要一个兼容 OpenAI Chat Completions 协议的模型服务地址很多国内云厂商都提供这类接口。关键是协议统一后面换模型不用改业务代码。2.3 环境变量和模型服务配置不要把 API Key 硬编码在代码里。推荐在项目根目录维护一个.env.example文件提交到 Git 时只保留模板不提交真实密钥。# .env 示例实际密钥不要提交到代码仓库 LLM_BASE_URLhttps://your-provider.example.com/v1 LLM_API_KEYsk-your-key-here LLM_MODELyour-model-name LLM_TEMPERATURE0.2 LLM_MAX_TOKENS2048 # Agent 运行参数 AGENT_MAX_ROUNDS5 AGENT_TIMEOUT_SECONDS60为什么temperature要设置得比较低Agent 在做工具调用时我们希望模型输出尽可能确定温度过高会让工具名称和参数出现随机性。一般工具调用场景建议设为 0 到 0.3创意写作场景才调高。在 Python 中读取环境变量时可以加上缺失校验避免启动后才发现配置错误import os def get_required_env(key: str) - str: value os.getenv(key) if not value: raise RuntimeError(f缺少必需的环境变量: {key}) return value BASE_URL get_required_env(LLM_BASE_URL) API_KEY get_required_env(LLM_API_KEY) MODEL get_required_env(LLM_MODEL) MAX_ROUNDS int(os.getenv(AGENT_MAX_ROUNDS, 5))2.4 最小项目结构先设计一个可以扩展的目录结构后面加工具、加记忆、加接口都会比较容易agent_demo/ ├── .env.example ├── requirements.txt ├── README.md ├── agent/ │ ├── __init__.py │ ├── core.py # Agent 主循环 │ ├── llm.py # 模型服务客户端 │ ├── memory.py # 短期记忆和会话管理 │ └── prompt.py # 系统 Prompt ├── tools/ │ ├── __init__.py │ ├── registry.py # 工具注册表 │ ├── calculator.py # 示例工具计算器 │ └── weather.py # 示例工具天气查询 └── main.py # 命令行入口这个结构的学习路径是先看registry.py理解“工具是什么”再看core.py理解“Agent 怎么循环”最后看main.py理解“怎么把 Agent 跑起来”。3. 手写一个最小 Agent不依赖框架先理解循环3.1 让模型学会“选择工具”手写 Agent 的关键是让模型输出能被程序可靠解析的“工具调用指令”。目前最常见的方案是让模型输出结构化 JSON程序解析后执行。一个简单的约定{ tool_calls: [ { name: calculator, arguments: { expression: (120 80) * 2 } } ] }使用 JSON 而不是让模型自由输出文本原因是解析稳定性。自由文本很难判断“模型到底想调用哪个工具、参数边界在哪里”JSON 有明确的键值结构出错也容易定位。如果你接的是 OpenAI 兼容接口可以直接使用接口原生的tools参数和tool_calls返回字段。下面为了讲清楚原理先用手写 JSON 的方案展示循环逻辑再在第四节用原生工具调用方案做进阶。3.2 定义一个工具注册表工具注册表解决两个问题一是给模型“有哪些工具可用”的定义二是让程序能根据工具名称找到对应执行函数。# tools/registry.py import inspect from typing import Any, Callable, Dict TOOL_REGISTRY: Dict[str, Callable] {} def register_tool(func: Callable) - Callable: 注册一个可被 Agent 调用的工具函数。 TOOL_REGISTRY[func.__name__] func return func def get_tool_schemas() - list[dict]: 生成 OpenAI 兼容的工具描述列表发给模型。 schemas [] for name, func in TOOL_REGISTRY.items(): sig inspect.signature(func) properties {} required [] for param_name, param in sig.parameters.items(): properties[param_name] { type: string, description: f参数 {param_name} } if param.default is inspect.Parameter.empty: required.append(param_name) schemas.append({ type: function, function: { name: name, description: (func.__doc__ or ).strip(), parameters: { type: object, properties: properties, required: required } } }) return schemas register_tool def calculator(expression: str) - str: 计算数学表达式比如 (120 80) * 2。只能处理安全的算术运算。 # 注意生产环境不要用 eval这里演示需要配合白名单校验 allowed set(0123456789-*/(). ) if not set(expression).issubset(allowed): return 错误表达式包含非法字符 try: result eval(expression) # 仅用于本地教学演示 return str(result) except Exception as exc: return f计算错误: {exc} register_tool def query_weather(city: str) - str: 查询指定城市的天气概况。 # 实际项目替换为真实天气 API 调用 mock_weather { 北京: 晴气温 3 到 12 摄氏度, 上海: 多云转小雨气温 10 到 16 摄氏度, 广州: 阴气温 18 到 24 摄氏度 } return mock_weather.get(city, f未找到 {city} 的天气数据)这里解释两个容易踩的坑eval只在教学场景可用。生产环境执行用户传入的表达式必须做白名单校验或者改用专用表达式解析库否则就是代码执行漏洞。工具函数的返回值必须是字符串或能被序列化成 JSON 的对象。模型看到的是文本结构化数据要自己转成字符串或 JSON。3.3 核心 Agent 循环实现Agent 主循环的本质是把消息列表发给模型 - 模型返回文本和工具调用 - 如果有工具调用程序执行并在消息列表里追加结果 - 再发给模型直到没有工具调用或达到上限。# agent/core.py import json from typing import Dict, List, Optional from agent.llm import chat_completion from tools.registry import TOOL_REGISTRY, get_tool_schemas SYSTEM_PROMPT 你是一个任务助手。根据用户的问题你可以调用工具来获取信息。 工具调用规则 1. 如果需要计算或查数据调用工具。 2. 一次可以调用多个工具但要确保参数完整。 3. 根据工具结果继续推理不要重复调用同一个已成功的工具。 4. 当得到最终结论时用中文直接回答用户不要再输出 tool_calls。 def execute_tool(name: str, arguments: Dict) - str: 根据工具名称找到函数并执行返回字符串结果。 func TOOL_REGISTRY.get(name) if not func: return f错误未知工具 {name} try: result func(**arguments) if isinstance(result, (dict, list)): return json.dumps(result, ensure_asciiFalse) return str(result) except TypeError as exc: return f错误工具参数不匹配 {exc} except Exception as exc: return f错误工具执行失败 {exc} def agent_loop(user_task: str, max_rounds: Optional[int] None) - Dict: max_rounds max_rounds or 5 messages [ {role: system, content: SYSTEM_PROMPT}, {role: user, content: user_task} ] for round_index in range(1, max_rounds 1): print(f[round {round_index}] 调用模型...) response chat_completion(messages, toolsget_tool_schemas()) assistant_message response[message] messages.append(assistant_message) tool_calls assistant_message.get(tool_calls, []) if not tool_calls: return { finished: True, rounds: round_index, answer: assistant_message.get(content, ) } for call in tool_calls: tool_name call[function][name] raw_arguments call[function][arguments] try: arguments json.loads(raw_arguments) if isinstance(raw_arguments, str) else raw_arguments except json.JSONDecodeError: arguments {raw: raw_arguments} print(f[round {round_index}] 调用工具: {tool_name}({arguments})) result execute_tool(tool_name, arguments) messages.append({ role: tool, tool_call_id: call.get(id, ftool_{round_index}), content: result }) return { finished: False, rounds: max_rounds, answer: 超过最大循环次数任务未完成。 }这个循环需要重点理解三个点第一消息列表是 Agent 的记忆载体。每一轮的工具调用和工具返回结果都追加到messages中模型才能看到“自己刚才做了什么、结果如何”。如果不清空或截断就会越积越长最终可能超出模型的上下文窗口。第二工具结果是给模型看的不是给用户看的。所以在追加tool消息时内容应该是结构化、完整的让模型能据此判断下一步。第三循环必须设上限。如果模型反复调用工具不结束会让成本失控也可能卡死在错误路径上。这里的max_rounds就是安全阀。3.4 模型客户端封装chat_completion是对模型服务的统一封装。以 OpenAI 兼容接口为例# agent/llm.py import os import requests def chat_completion(messages: list, tools: list | None None): url f{os.getenv(LLM_BASE_URL, https://your-provider.example.com/v1)}/chat/completions payload { model: os.getenv(LLM_MODEL), messages: messages, temperature: float(os.getenv(LLM_TEMPERATURE, 0.2)), max_tokens: int(os.getenv(LLM_MAX_TOKENS, 2048)), tools: tools or [], tool_choice: auto } headers { Authorization: fBearer {os.getenv(LLM_API_KEY)}, Content-Type: application/json } resp requests.post(url, jsonpayload, headersheaders, timeout60) resp.raise_for_status() data resp.json() return { message: data[choices][0][message], usage: data.get(usage, {}) }这里不写死某一家的地址而是用环境变量注入基础地址。如果你的模型服务不兼容tools参数可以暂时改用“模型输出 JSON 字符串”的方案让模型直接输出包含tool_calls的 JSON再用json.loads解析。3.5 最小验证在main.py里放一个命令行入口# main.py import os from dotenv import load_dotenv from agent.core import agent_loop load_dotenv() if __name__ __main__: task input(请输入任务) result agent_loop(task, max_roundsint(os.getenv(AGENT_MAX_ROUNDS, 5))) print(\n最终回答) print(result[answer])运行cd agent_demo pip install python-dotenv requests python main.py输入这样的任务请帮我计算 (120 80) * 2然后查询北京的天气。预期日志会显示 Agent 先调用calculator再调用query_weather最后输出一段结合两个工具结果的中文回答。如果模型在一次回复中没有同时生成两个工具调用它也会在下一轮继续执行未完成的第二个工具。可以看到手写方案代码量不大但它已经具备了 Agent 最核心的骨架。这个骨架在后面的框架方案里本质上也还是这套消息循环。4. 用 LangChain / LangGraph 实现知识库型 Agent4.1 为什么需要框架手写 Agent 能让你理解原理但进入生产时还是有很多重复工作要处理文档切分、向量化、存储和检索。多个工具定义的标准化。会话状态持久化。图状态流转比如“先检索再回答”“失败后重试”。与外部数据库、消息队列、可观测系统的集成。这时候使用框架能显著提高开发效率。下面以 LangChain 和 LangGraph 为例说明代码保留常见写法的核心结构。由于这类框架版本更新频繁落地前一定要以当前官方文档为准。4.2 场景设定知识库问答 Agent很多企业做 Agent 的第一个场景就是“文档问答”让用户用自然语言提问Agent 从企业知识库里检索相关片段再结合模型生成回答。核心链路是把企业文档加载进来按一定粒度切分成片段。把片段向量化后存入向量数据库。用户提问后先用向量检索找到相关片段。把片段作为上下文交给模型生成回答。如果知识库内容不足Agent 可以调用外部接口补全信息。这个场景也回应了很多人关心的“Obsidian / 本地笔记 AI Agent 知识库”如何落地本质都是“文档加载 - 切分 - 向量化 - 检索 - 问答”只是不同客户端负责文档接入和交互。4.3 文档加载和切分from langchain_community.document_loaders import TextLoader from langchain_text_splitters import RecursiveCharacterTextSplitter loader TextLoader(docs/faq.txt) documents loader.load() splitter RecursiveCharacterTextSplitter( chunk_size500, chunk_overlap50, separators[\n\n, \n, 。, , , , , , ] ) chunks splitter.split_documents(documents) print(f文档被切分为 {len(chunks)} 个片段)切分参数为什么重要chunk_size太小单个片段上下文不足检索后模型看不到完整语义。chunk_size太大向量检索精度下降而且容易超出模型上下文限制。chunk_overlap保留前后文衔接避免语义被切断在边界处。中文场景最好在分隔符里加入中文标点否则切分时可能把一个完整的句子拆开。4.4 构建向量检索工具from langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import FAISS embedding OpenAIEmbeddings( base_urlos.getenv(LLM_BASE_URL), api_keyos.getenv(LLM_API_KEY), modelyour-embedding-model ) vector_store FAISS.from_documents(chunks, embedding) retriever vector_store.as_retriever(search_kwargs{k: 3}) register_tool def search_knowledge(query: str) - str: 在企业知识库中检索与 query 相关的文档片段。 docs retriever.invoke(query) if not docs: return 知识库中未找到相关内容。 return \n\n---\n\n.join([doc.page_content for doc in docs])这个工具一旦注册进 Agent模型就会在回答前先调用search_knowledge把检索结果作为判断依据。向量数据库选择方面小型原型用 FAISS 或 Chroma 就够生产环境建议使用 pgvector、Milvus、Qdrant 等支持水平扩展和高可用的方案。4.5 用 LangGraph 控制流程如果你希望流程更可控可以用 LangGraph 显式定义节点和边from typing import TypedDict from langgraph.graph import StateGraph, END class AgentState(TypedDict): question: str context: str answer: str def retrieve_node(state: AgentState): docs retriever.invoke(state[question]) return {context: \n\n.join([d.page_content for d in docs])} def generate_node(state: AgentState): prompt f基于以下资料回答问题\n\n{state[context]}\n\n问题{state[question]} response chat_completion( [{role: user, content: prompt}], tools[] ) return {answer: response[message].get(content, )} graph StateGraph(AgentState) graph.add_node(retrieve, retrieve_node) graph.add_node(generate, generate_node) graph.set_entry_point(retrieve) graph.add_edge(retrieve, generate) graph.add_edge(generate, END) app graph.compile()把流程拆成节点有两个好处。一是每一步可以独立测试比如单独验证检索节点返回的内容是否相关。二是每步可以插入人工审核、限流、日志埋点和异常处理这是生产环境必须的。需要注意的是这里不是“必须用 LangGraph”。如果团队已经熟悉自研编排完全可以实现同样的状态机。框架只是减少重复代码不能替你设计流程。4.6 关键参数速查框架方案里经常需要调参整理成一张速查表会更方便参数默认值示例作用调大的影响调小的影响chunk_size500每个片段的字符数上下文更完整检索精度可能下降上下文更聚焦容易截断语义chunk_overlap50相邻片段重叠字符衔接更连续存储和计算量增大可能丢失边界语义k检索条数3每次返回的文档片段数上下文更充分成本和时间上升响应更快可能漏信息temperature0.2采样随机性回答更多样不稳定回答更确定适合工具调用max_rounds5Agent 最大循环轮数能完成更复杂任务成本和风险上升快速终止复杂任务可能失败这些参数不是一次调完就固定不变。实际项目需要根据领域文档、问题类型和模型版本反复实验最好把实验过程和结果记录到评估用例中。5. Spring Boot 侧接入 AI Agent 客户端5.1 Java 技术栈为什么要关心 Agent很多企业核心系统是 Spring Boot 写的Agent 服务往往是 Python 或独立服务这时候 Java 侧需要一个稳定、可观测的“Agent 客户端”。职责包括把用户请求转发给 Agent 服务。管理 HTTP 连接、超时、重试。解析统一响应结构。把 Agent 运行日志接入原有监控体系。对客户端做限流和降级。这在架构上又叫“AI Agent 网关层”。Java 侧不直接调用大模型而是面向内部 Agent 服务封装统一接口这样前后端职责清晰也方便后面替换 Agent 实现。5.2 定义统一的接口响应结构无论后端 Agent 服务是 Python 还是 Java 实现建议接口响应统一为{ task_id: task_20260201_001, status: SUCCESS, answer: 订单总额为 400 元其中包含 80 元税费。, rounds: 3, used_tools: [calculator, query_policy], error_message: }统一结构的好处是下游客户端不需要关心 Agent 内部有多少轮调用。客户端只关心状态、答案、任务ID和错误信息。对应 Java 实体可以用一个 Record 表示public record AgentResponse( String taskId, String status, String answer, int rounds, ListString usedTools, String errorMessage ) { public boolean isSuccess() { return SUCCESS.equals(status); } }5.3 用 RestClient 封装 HTTP 调用Spring Boot 3.2 以后内置了RestClient比传统的RestTemplate更简洁。示例Service public class AgentClient { private final RestClient restClient; public AgentClient(RestClient.Builder builder, Value(${agent.service.url}) String agentServiceUrl) { this.restClient builder.baseUrl(agentServiceUrl).build(); } public AgentResponse sendTask(String userId, String question) { MapString, Object request Map.of( user_id, userId, question, question ); try { return restClient.post() .uri(/agent/task) .header(X-User-Id, userId) .body(request) .retrieve() .body(AgentResponse.class); } catch (HttpClientErrorException ex) { throw new AgentServiceException(Agent 服务返回客户端错误, ex); } catch (HttpServerErrorException ex) { throw new AgentServiceException(Agent 服务暂时不可用, ex); } } }这里有几个工程细节要注意agent.service.url不要写死在代码里通过配置中心或环境变量注入。要区分客户端错误 4xx 和服务端错误 5xx方便后续告警。不要把大模型密钥下发给前端前端只能访问你的 Spring Boot 服务不能直接访问 Agent 服务。5.4 超时、重试和降级调用远程 Agent 服务不能没有超时控制。一个 Agent 任务可能要跑 5 到 15 秒超时时间需要比普通 HTTP 接口更长但也不能无限等待。spring: threads: virtual: enabled: true agent: service: url: http://agent-service:8080 connect-timeout: 3s read-timeout: 30s在配置类中设置Bean public RestClient agentRestClient(RestClient.Builder builder, Value(${agent.service.url}) String url) { return builder .baseUrl(url) .requestFactory(getRequestFactory()) .build(); } private ClientHttpRequestFactory getRequestFactory() { SimpleClientHttpRequestFactory factory new SimpleClientHttpRequestFactory(); factory.setConnectTimeout(3000); factory.setReadTimeout(30000); return factory; }如果选用 Spring AI 的客户端封装思路是一样的只是它帮你处理了聊天消息、工具定义和模型供应商协议。下面是一个基于 Spring AI 的典型写法Component public class SpringAiChatAgent { private final ChatClient chatClient; public SpringAiChatAgent(ChatClient.Builder builder) { this.chatClient builder.build(); } public String chat(String userMessage) { return chatClient.prompt() .user(userMessage) .call() .content(); } }注意 Spring AI 的模块和 API 变化很快不同版本对 OpenAI、通义等模型服务商的依赖与配置类不完全一致。实际接入前要核对当前版本的官方文档不要把网上一个月前的代码直接复制进生产环境。5.5 Java 侧的异常处理无论使用哪种客户端都要有一个统一异常处理避免把底层连接池、超时异常直接抛给前端RestControllerAdvice public class AgentExceptionHandler { ExceptionHandler(AgentServiceException.class) public ResponseEntityMapString, Object handleAgentServiceException(AgentServiceException ex) { return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE) .body(Map.of( code, AGENT_SERVICE_ERROR, message, ex.getMessage() )); } }6. 运行验证与结果分析6.1 只验证“能启动”远远不够不少 Agent 项目在开发阶段只测试了“用户提问 - 模型回答”这一条正常路径上线后才暴露出工具参数错误、知识库检索为空、超时、上下文爆炸等问题。完整的验证至少覆盖四类场景正常路径Agent 按预期调用工具并给出正确回答。工具失败路径工具返回错误Agent 是否重试、换工具还是直接给出错误结论。拒绝路径用户提问不在允许范围内Agent 是否安全拒绝。边界路径问题超长、并发过高、服务超时、会话中断。6.2 关键测试用例表格在设计测试用例时可以参考下面的结构用例类型输入示例预期行为失败表现单工具调用计算 (120 80) * 2调用 calculator返回 400模型直接口算或格式错误多工具调用查北京的天气并计算温度换算依次调用 weather 和 calculator只完成其中一个工具参数错误调用 calculator 时缺参数Agent 自动补齐或报参数错误工具执行异常链路断裂知识库检索为空询问知识库中不存在的内容Agent 明确说未找到不编造模型幻觉编造答案超过最大轮数连续不结束的复杂任务达到 max_rounds 后强制结束任务无限执行成本失控远程服务超时模型服务慢响应客户端在 read-timeout 后收到错误提示请求挂起线程池被占满6.3 日志设计让每一轮调用都可追踪自研 Agent 要特别注意日志。没有日志Agent 一旦回答错误你根本不知道是模型决策错了、工具参数传错了还是工具结果本身有问题。推荐在每个关键节点输出结构化日志import logging logging.basicConfig(levellogging.INFO, format%(asctime)s %(levelname)s %(message)s) logger logging.getLogger(agent) logger.info(begin_task task_id%s question%s, task_id, user_task) logger.info(tool_call round%s tool%s args%s, round_index, tool_name, arguments) logger.info(tool_result round%s tool%s result%s, round_index, tool_name, result) logger.info(finish_task task_id%s rounds%s answer%s, task_id, round_index, final_answer)为什么日志要包含task_id因为一个用户问题可能被多个微服务处理只有全局任务 ID 才能在日志系统里把入口、模型调用、工具调用串成一条完整链路。生产环境还会配合 OpenTelemetry 等链路追踪工具把 Agent 内部步骤和 Spring Boot 侧调用都纳入 trace。6.4 回归测试怎么准备Agent 输出天然带有随机性回归测试不能只比对“字符串完全一致”。建议准备一组固定测试集包含正确答案关键词或结论判定规则。对工具调用顺序做断言比如“必须先调用 search_knowledge 再生成回答”。对 JSON 解析、工具执行这类确定性逻辑做精确断言。使用较低 temperature0 到 0.2降低随机性。定期人工抽检新版本模型或框架升级后的输出质量。7. 常见问题排查7.1 排查顺序Agent 问题比普通接口更难排查因为它涉及模型、工具、框架和网络四层。建议按下面顺序定位确认输入和使用场景是否符合预期。确认模型服务和 API Key 是否可用。确认消息列表和上下文是否被正确传递。确认工具调用参数是否完整、解析是否正确。确认工具执行结果是否被正确回填给模型。查看日志中的每一轮tool_call和tool_result。确认是否触发了最大轮数或超时限制。检查框架版本和依赖是否与代码示例一致。7.2 高频问题与处理方案问题现象常见原因检查方式处理建议调用模型接口报 401 或 403API Key 无效或未注入环境变量检查环境变量、服务商控制台重新生成密钥确保 Secret 不泄露Agent 不调用工具直接回答系统 Prompt 未明确工具规则或tools参数未传入打印请求 payload 中的 tools在 Prompt 中说明“需要计算或查数据时必须调用工具”工具调用参数解析失败模型输出 JSON 不合法或字段缺失打印raw_arguments原始字符串增加 JSON 解析兜底失败时告诉模型重新生成工具执行报参数不匹配工具函数参数名或类型与模型生成不一致对比 schema 定义和实际请求参数用清晰的工具函数名和参数描述必要时做参数补全上下文长度超限多轮工具调用导致消息列表过长统计请求 token 用量启用摘要压缩或只保留最近 N 轮工具结果Agent 陷入死循环模型反复调用同一个工具观察日志中的 round 分布设置 max_rounds检测重复工具调用并主动终止知识库检索结果不相关chunk 切分过大或 embedding 模型不合适打印检索到的 document 内容调整 chunk_size换向量模型或增加重排模型Spring Boot 读不到配置环境变量名或 yaml 层级写错查看启动日志配置绑定信息使用ConfigurationProperties校验配置映射Agent 任务超时模型响应慢或工具执行慢看模型接口耗时分位值提高 read-timeout改用异步任务增加限流7.3 一个典型日志排查案例假设用户问“北京到上海的距离是多少”Agent 却回答说“北京天气晴”。日志中可能出现[round 1] 调用工具: query_weather({city: 北京}) [round 1] 工具结果: 晴气温 3 到 12 摄氏度 [round 2] 调用工具: query_weather({city: 上海}) [round 2] 工具结果: 多云转小雨气温 10 到 16 摄氏度 [round 3] 没有工具调用模型直接回答从日志看Agent 调用了两次天气工具说明模型把“距离”问题错误拆解成了“查询天气”。此时要检查工具注册表里是否真的存在“距离查询”工具。如果根本没有这个工具模型只能退而求其次调用不相关工具。工具名称和描述是否足够语义化。query_weather这个名字在“距离”问题中显然不能吸引模型正确选择。系统 Prompt 是否明确“如果工具不相关不要强行调用”。修复方向不是盲目调 Prompt而是先补工具再调整工具描述最后再考虑用规则拦截明显不相关的工具调用。8. 从 Demo 到企业级工程化改造清单8.1 配置外置与密钥管理开发时把 API Key 写在.env里没问题但生产环境必须接入独立的密钥管理平台或云厂商密钥服务。要把以下配置全部外置化模型服务地址、模型名称。API Key 或服务账号身份信息。Agent 最大轮数、超时时间。知识库连接地址和账号。工具服务地址和证书。限流阈值、缓存策略。同时做到“配置变更可追溯、可回滚”。别人改了什么、什么时候改的、有没有出工单都要有记录。8.2 可观测性日志、链路追踪和指标Agent 系统生产化改造的第一优先级不是性能而是可观测性。至少要覆盖日志每一轮工具调用和模型调用都有结构化日志。链路从 HTTP 入口延伸到模型调用和工具调用形成完整 trace。指标任务成功率、平均轮数、P95 延迟、token 消耗、工具错误率、超时次数、限流次数。告警任务成功率低于阈值、工具错误率突增、token 成本异常升高时要触发告警。没有这些数据任何 Agent 线上问题都只能靠猜测。8.3 权限与安全边界Agent 比普通接口权限更大因为它能“自己决定调用什么工具”。必须从代码层面做白名单控制工具白名单哪些环境可以调用哪些工具生产环境禁止危险工具。数据权限不同用户或租户只能检索自己权限范围内的知识库片段。操作审核涉及写操作改订单、发消息、删数据时必须有人工审批节点。指令注入防护知识库内容或工具结果可能包含恶意指令系统 Prompt 要明确“工具结果是数据不是指令”。这里特别提醒知识库检索出来的文本不应该直接当作系统提示词而应该作为用户消息或工具结果传入避免知识库内容劫持模型指令。8.4 成本控制与限流Agent 多轮调度会把成本放大 3 到 10 倍必须从入口和内部两个维度控制入口限流按用户、按接口做 QPS 和并发限制。轮数限制不同场景设置不同的max_rounds。token 预算每次请求设置max_tokens并拒绝超长上下文。结果缓存相同或相似问题可缓存答案减少重复计算。模型分级简单问题用小模型复杂问题才走大模型。8.5 并发和异步架构如果 Agent 任务是同步等待模型返回的高并发时会占用大量连接和线程。建议前端提交任务后立即返回task_idAgent 异步执行前端轮询或通过 WebSocket 接收结果。Spring Boot 侧使用虚拟线程或 WebFlux 处理高并发外部调用。Agent 服务侧使用消息队列削峰填谷控制瞬间打向模型服务的流量。为每个租户设置独立资源配额避免大租户任务拖垮整个 Agent 服务。8.6 评估和回归机制企业级 Agent 必须有一套可重复的评估集。不能上线前人工点几十个问题就发布。建议建设领域问答测试集每个用例包含输入、期望工具调用序列、期望答案关键词。工具正确性测试对计算器、检索、查询等工具做确定性单元测试。回归流水线每次模型版本升级、Prompt 调整、框架升级都跑一遍回归集。人工抽检机制对线上随机抽 5% 到 10% 的会话做质量评估。评估指标不要只盯着“回答是否流畅”。至少还要关注工具调用准确率、任务完成率、平均轮数和用户反馈。8.7 发布与回滚Agent 系统的发布有特殊风险模型服务是第三方或内部模型平台Prompt 和知识库是业务逻辑代码只是编排层。任何一个输入变化都可能影响输出。发布前检查清单模型服务是否已灰度验证是否有备用模型。新 Prompt 是否跑过回归测试集。知识库变更是否经过版本管理。工具变更是否同时更新了版本号和接口文档。是否配置了降级策略模型服务异常时是否返回固定兜底文案。是否保留了上一个版本的核心配置和 Prompt 快照。回滚不是只有代码回滚。Prompt、知识库、工具列表、模型版本都要支持回滚否则出现问题后无法快速恢复。结尾从手写循环到生产体系AI Agent 开发最核心的能力是理解“模型输出不确定性”和“工程确定性”之间的结合点。模型负责生成决策但代码负责约束、验证、记录和兜底。这篇文章从手写 Agent 循环讲到了 LangChain / LangGraph 知识库 Agent再到 Spring Boot 客户端集成和上线前的工程化改造整条链路的关键判断只有一个不要让大模型直接暴露在业务边界上而是把工具调用、权限、日志、限流和回滚都变成可管理的工程代码。对刚入门的人建议先把你自己的 Agent 循环跑通然后自己加一个数据库查询工具再做知识库检索最后接入 Spring Boot 接口。对已经在做生产 Agent 的人建议优先补齐可观测性和评估集没有这两项后续所有优化都会失去方向。下一步可以考虑深入研究 ReAct 模式、多智能体协作、记忆持久化和评估系统设计这些方向都是在本文所述骨架上的自然延伸。