ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从零手写AI Agent:核心循环与5天学习路径

从零手写AI Agent:核心循环与5天学习路径 在实际工程中AI Agent 的核心价值不是“会聊天”而是“能干活”。它接收自然语言任务后自己拆解步骤、调用工具、读取结果再决定下一步怎么做。很多开发者刚开始学 AI Agent 时会误以为框架越复杂越好实际上先把“模型 工具 循环”这三个最小要素跑通后面再学 LangChain、AutoGen、Hugging Face smolagents 都会轻松很多。这套教程按一条可执行的 5 天学习路径展开先理解原理再准备环境接着徒手做一个能分析日志的极简 Agent最后把它改造成适合上线的结构。1. 先理解 AI Agent 是什么再动手写代码1.1 用一句话定义 AI AgentAI Agent 是一个由大模型驱动的、可以自主调用外部工具并基于结果继续推理的程序。它不是一个聊天窗口而是一个任务执行系统。用户给一个目标Agent 自己判断需要哪些信息调用对应的函数、接口、数据库或命令行工具把拿到的结果重新交给模型直到最终完成任务。很多人把 Agent 理解成 Chatbot这是第一个误区。Chatbot 只负责生成回答Agent 要负责完成任务。任务通常不能被一次回答完成必须查询日志、访问数据库、调用接口、读取文件。所以 Agent 的代码复杂度不在于“写提示词”而在于把模型输出转成真实行动再把行动结果送回模型。1.2 核心循环感知、决策、行动、观察理解 Agent 的关键是掌握它的运行循环。一次 Agent 调用并不是“用户提问模型回答”而是下面这个循环用户输入任务系统把任务放入消息列表。模型阅读消息决定是直接回答还是调用一个工具。如果模型决定调用工具它会输出工具名称和参数。系统执行工具拿到真实结果。系统把工具结果作为新消息追加到消息列表。模型继续推理可能再次调用工具也可能输出最终回答。这个过程可以简单表达为user: 请分析今天日志中的 ERROR assistant: 我需要查询 ES调用 query_es_logs(keywordERROR, minutes60) tool: 返回 10 条日志 assistant: 在这些日志中最常见的是数据库连接超时……模型本身没有执行能力只能输出文本。Tool Call 机制让模型在文本中声明“我要调用哪个函数、参数是什么”真正执行由你的代码完成。这也是 Agent 与普通模型 API 调用的最大区别普通调用一次问答结束Agent 是多次问答和工具执行的循环。1.3 Agent、RAG、Workflow 的关系很多初学者会把 Agent、RAG、Workflow 混在一起。它们解决的问题并不相同但可以组合使用。概念解决什么问题执行路径典型场景RAG模型不知道私有知识检索后拼接上下文再生成文档问答、知识库检索Workflow流程固定按照预先定义步骤执行数据清洗、定时报表Agent流程开放需要动态决策模型决定下一步日志分析、故障排查、自动化运维用一个例子区分如果只是“每天定时查询日志并生成报表”用 Workflow 更稳定如果是“用户随机提一个运维问题Agent 自己决定查日志还是查监控”这才是 Agent 的典型场景。实际项目中Agent 也可以调用 RAG 查询接口也可以把高风险步骤交给 Workflow 控制两者并不冲突。2. 学习 AI Agent 前的环境准备2.1 三种技术栈如何选学习环境建议使用 Python因为模型调用、数据处理、脚本调试的生态最直接。如果所在团队是 Java 技术栈可以直接研究 Spring AI、LangChain4j 这类 Java 生态方案如果主要做前端则可以用 Node.js 或 TypeScript 实现 Agent 后端再通过 SSE 或 WebSocket 把结果推给页面。技术栈适合人群学习重点PythonAI 算法、数据工程、脚本开发模型 API、工具调用、数据处理Java后端团队、企业级服务Spring 集成、线程池、异步处理Node.js / TypeScript前端全栈开发者流式输出、接口封装、前端集成建议只选一条主线学完不要第一步就在三种语言里来回切换。下面示例以 Python 3.10 为例思路同样适用于 Java 和 Node.js。2.2 模型接口与本地模型的取舍学 Agent 必须有一个能稳定支持函数调用的模型服务。函数调用也叫 Tool Call、Function Calling是 Agent 的核心能力。模型需要根据用户问题输出结构化工具参数而不是只生成自然语言。使用方式优点注意点云端模型 API工具调用效果好接入快需要管理密钥、关注费用和限流本地模型数据不出内网可控性高需要 GPU 资源工具调用能力可能弱混合模式敏感任务走本地常规任务走云端路由逻辑要稳定便于切换如果你是初学者先使用云端模型 API 跑通流程。选择服务商时优先确认接口是否兼容chat/completions风格因为很多框架默认按这种协议封装。如果公司要求数据不能出内网再评估本地模型。本地模型部署成本高而且参数较小的模型在工具调用上的稳定性明显弱于大模型调试成本会高很多。2.3 最小项目结构建议先建立一个最小项目结构避免把所有代码堆在一个文件里ai-agent-lab/ ├── .env ├── requirements.txt ├── agent.py ├── tools/ │ ├── __init__.py │ └── es_log.py └── logs/ └── agent.log.env文件保存模型服务地址、密钥、ES 地址等配置不要提交到代码仓库。agent.py负责 Agent 主循环tools目录放工具函数logs目录放运行日志。2.4 环境检查清单在写代码前先确认以下内容Python 版本是否为 3.10 或更高。是否安装requests、python-dotenv。模型服务地址、API Key、模型名称是否已写入.env。Elasticsearch 服务是否已启动索引名是否可访问。工具函数是否可以不依赖 Agent 单独运行。可以用两条命令快速检查环境python --version curl -X GET http://localhost:9200/_cluster/health?pretty如果 ES 返回status: green或yellow说明服务正常。如果连接不通先解决 ES 服务问题再继续写 Agent 代码。3. 从零手写一个能分析 ES 日志的极简 Agent3.1 先封装模型调用不引入框架先用 Python 封装一个模型调用函数。这里默认模型服务提供chat/completions接口实际使用时替换成你的服务地址和模型名。import os import requests from dotenv import load_dotenv load_dotenv() LLM_API_URL os.environ.get(LLM_API_URL) LLM_API_KEY os.environ.get(LLM_API_KEY) LLM_MODEL os.environ.get(LLM_MODEL) def chat_completion(messages, toolsNone): payload { model: LLM_MODEL, messages: messages, tools: tools or [], } resp requests.post( f{LLM_API_URL}/chat/completions, headers{ Authorization: fBearer {LLM_API_KEY}, Content-Type: application/json }, jsonpayload, timeout30 ) resp.raise_for_status() return resp.json()[choices][0][message]这段代码把模型返回的message原样返回其中可能包含tool_calls字段。tools参数不是所有模型服务都支持如果服务只支持旧版functions参数需要在这一层做适配。封装模型调用的意义在于后面换模型服务时只需要改这一个文件。3.2 实现 Agent 主循环主循环是 Agent 的核心负责判断模型是要继续调用工具还是输出最终答案。def run_agent(task, tools): messages [{role: user, content: task}] max_steps 5 for step in range(max_steps): msg chat_completion(messages, toolstools) messages.append(msg) if not msg.get(tool_calls): return msg.get(content) for call in msg[tool_calls]: name call[function][name] arguments call[function][arguments] print(f[step {step 1}] 调用工具: {name}, 参数: {arguments}) result call_tool(name, arguments) messages.append({ role: tool, tool_call_id: call[id], content: result, }) raise RuntimeError(Agent 执行超过最大步数)这里有几个关键点。第一必须设置max_steps否则模型可能无限循环。第二工具结果必须通过tool_call_id和模型输出关联顺序不能乱。第三工具结果要转成字符串因为模型接收到的是文本不是结构化对象。3.3 编写 ES 日志查询工具下面实现一个通过 ES REST API 查询日志的工具函数。它接收关键词和时间范围返回最近命中的日志。import json import requests ES_URL os.environ.get(ES_URL, http://localhost:9200) ES_INDEX os.environ.get(ES_INDEX, app-logs-*) def query_es_logs(keyword: str, minutes: int 30) - str: query { size: 10, sort: [{timestamp: {order: desc}}], query: { bool: { must: [{match: {message: keyword}}], filter: [{range: {timestamp: {gte: fnow-{minutes}m}}}] } } } resp requests.post( f{ES_URL}/{ES_INDEX}/_search, jsonquery, headers{Content-Type: application/json}, timeout15 ) resp.raise_for_status() hits resp.json().get(hits, {}).get(hits, []) return json.dumps([hit[_source] for hit in hits], ensure_asciiFalse)返回类型是字符串而不是 Python 字典。这样做是为了避免 Agent 主循环在处理工具结果时出现类型混乱。实际项目中还要考虑 ES 认证、超时、分页、字段裁剪以及索引不可用时的兜底。3.4 工具注册表与 Tool Schema要让模型知道有哪些工具必须提供 Tool Schema。下面是一个query_es_logs的 Schema 示例{ type: function, function: { name: query_es_logs, description: 根据关键词和最近时间范围查询 ES 中的应用日志, parameters: { type: object, properties: { keyword: { type: string, description: 要搜索的日志关键词 }, minutes: { type: integer, description: 最近多少分钟默认 30 } }, required: [keyword] } } }同时需要一个工具注册表把工具名称和函数映射起来TOOL_REGISTRY { query_es_logs: query_es_logs, } def call_tool(name, arguments_json): if name not in TOOL_REGISTRY: return f未知工具: {name} func TOOL_REGISTRY[name] args json.loads(arguments_json) try: return str(func(**args)) except Exception as e: return f工具执行失败: {type(e).__name__}: {e}Schema 里的description必须写清楚模型靠描述决定什么时候调用工具。required字段不能省否则模型可能遗漏必填参数。3.5 运行与验证在agent.py底部加入启动代码if __name__ __main__: tools [ES_LOG_TOOL_SCHEMA] task 统计最近30分钟日志中的 ERROR 数量并告诉我出现最多的服务 result run_agent(task, tools) print(result)正常输出会类似[step 1] 调用工具: query_es_logs, 参数: {keyword: ERROR, minutes: 30} 查询到 10 条日志其中 order-service 出现 4 次payment-service 出现 3 次……如果模型直接输出文字而不调用工具先检查模型服务是否支持tools参数然后检查 Schema 里的工具名和描述是否清晰。如果工具参数解析失败打印tool_calls原始内容确认模型实际生成的结构。4. 主流框架与选型4.1 框架定位当 Agent 逻辑变复杂后手写代码的维护成本会上升。此时可以引入框架但不要盲目追求框架。常见框架大致分几类LangChain / LangGraph偏重流程编排和状态管理适合复杂 Agent。LlamaIndex偏重 RAG 和文档检索适合知识问答。Hugging Face smolagents轻量级适合教学和快速原型。AutoGen / CrewAI偏重多 Agent 协作和角色分工。Spring AI / LangChain4j面向 Java 技术栈。框架不是必需品。手写最小实现能让你理解 Agent 的底层逻辑框架则能帮你处理记忆、回调、追踪、多工具调度等工程问题。4.2 选型对比框架擅长场景上手成本注意事项LangChain / LangGraph复杂链、状态图、多工具中高版本变化快先锁定版本再开发LlamaIndexRAG 和文档问答中索引和检索概念多Hugging Face smolagents轻量 Agent 教学与快速原型低依赖模型工具调用能力AutoGen / AG2多 Agent 协作中高多智能体调试成本高CrewAI角色化任务团队低中生产稳定性需要自己补Spring AI / LangChain4jJava 团队集成中和 Spring 生态结合紧密4.3 选型建议先手写最小实现再迁移到框架。迁移时保留工具函数不变只替换 Agent 调度层这样风险最小。国内开发者在选型时优先看文档是否有中文、模型服务是否兼容常用工具调用协议、社区里能否搜到同类问题。团队是 Java 技术栈就选 Spring AI 或 LangChain4j团队做 AI 应用选 LangGraph 或 smolagents团队做前端可以用 Node.js 生态并自己封装一层 Agent 服务。注意框架版本迭代很快不要在没确认版本的情况下直接复制网上的代码。先锁定版本号再写业务代码。5. 把 Agent 从单文件改造成 Skill 化结构5.1 什么是 Agent SkillSkill 是把一组工具、提示词和配置打包成可复用单元的一种组织方式。实际项目中日志分析、故障排查、代码检查、数据库查询都可以沉淀为 Skill。模型通过描述文件知道“什么时候用这个技能、需要哪些参数、返回什么结果”。Skill 化的好处是复用和隔离。日志分析团队维护日志分析 Skill运维团队维护故障排查 SkillAgent 主程序只需要按名称加载即可。5.2 目录结构一个日志查询 Skill 可以组织成logs-agent/ ├── agent.py ├── requirements.txt └── skills/ └── es-log-query/ ├── SKILL.md └── query.pySKILL.md描述技能用途query.py放具体实现。主程序启动时扫描skills目录把每个 Skill 的描述注册给模型。5.3 SKILL.md 示例--- name: es_log_query description: 查询 Elasticsearch 日志适合错误分析、服务可用性排查。 parameters: keyword: type: string description: 日志关键词 minutes: type: integer default: 30 --- # ES 日志查询 当用户需要分析错误日志时使用该技能。 查询结果按时间倒序返回最多返回 10 条。这个示例不是严格标准而是为了说明 Skill 的形态。不同平台对 Skill 的格式有不同规范实际使用时应以所选平台文档为准。核心思路是让工具描述和工具实现分离使模型更容易发现工具、开发者更容易维护工具。5.4 Workflow 和 Agent 的分工模式适用场景优点缺点Workflow流程固定、步骤明确稳定、可预测不适合开放式任务Agent目标开放、路径多变灵活不确定性强、需治理实际项目中不要把敏感且不可逆的动作直接交给 Agent 自动执行。例如删除索引、上线发布、批量更新数据应该先让 Agent 生成操作方案再由 Workflow 或人工审批执行。6. 从学习 Demo 到生产级 Agent6.1 结构化日志与链路追踪Agent 的每一次工具调用、模型结果、Token 消耗都应该记录到日志。一次任务可能会进行多轮工具调用必须用trace_id把这些步骤串联起来。{ timestamp: 2026-01-01T10:00:00Z, trace_id: abc123, agent_id: log-analyzer, step: 2, tool: query_es_logs, input: {keyword: ERROR, minutes: 30}, output_size: 2048, latency_ms: 312 }没有链路追踪Agent 一旦出错就只能看到“结果不对”无法定位是哪一步出了问题。6.2 超时、重试、熔断和限流模型服务和 ES 服务都可能超时。对 Agent 而言超时和重试需要分层控制层级配置建议模型 API超时 30 秒失败重试 2 次ES / 工具接口超时 15 秒重试策略要避开写入类操作Agent 主循环最大步数 5 到 10 步并发控制限制同时运行的 Agent 任务数重试需要配合指数退避避免故障期间产生大量请求。限流可以在入口层做也可以在模型调用层做。生产环境如果模型服务持续报错应该触发熔断而不是继续发起无效请求。6.3 接口、密钥和权限安全不要把模型 API Key 放到前端代码里。浏览器环境中的密钥很容易泄露应该由后端统一持有模型服务配置。Agent 的执行结果可以通过后端接口返回给前端前端只负责展示。工具权限也要严格限制。如果 Agent 要访问 ES建议使用只读账号如果 Agent 要操作数据库建议使用最小权限账号如果 Agent 要执行命令或发布操作必须加入人工审批环节。6.4 不同技术栈的接入重点Python 技术栈关注点是异步和数据处理。Java 技术栈关注线程池和 Spring 生态。以 Spring MVC 为例Agent 执行是耗时操作不应阻塞主线程可以用SseEmitter把结果推给前端PostMapping(/agent/run) public SseEmitter run(RequestBody TaskRequest request) { SseEmitter emitter new SseEmitter(); agentExecutor.execute(() - { try { AgentResult result agentService.run(request.getTask()); emitter.send(result.output()); emitter.complete(); } catch (Exception e) { emitter.completeWithError(e); } }); return emitter; }前端接入时使用fetchReadableStream或EventSource消费流式输出。不要把模型密钥放进页面所有请求应该走后端代理。6.5 发布前检查清单正式发布前至少检查以下内容模型服务账号是否可用额度是否充足。Agent 可调用的工具白名单是否已配置。所有第三方接口是否有超时、重试和异常兜底。日志中是否包含trace_id和完整工具调用记录。上下文是否会无限制增长是否需要截断。敏感操作是否已加入人工审批流程。返回给用户的内容是否经过基本校验。7. 5 天从入门到精通的练习路径7.1 五天安排天数主题交付物验收标准Day 1Agent 概念与环境准备跑通普通模型对话能打印模型返回内容Day 2函数调用与工具模型能调用一个工具工具结果能回到模型Day 3极简 Agent完成日志分析 Agent能回答日志问题Day 4框架迁移把实现迁移到框架功能一致且增加追踪Day 5生产化接入接口、日志和监控能处理并发和错误7.2 每天验收标准Day 1 的验收标准不是“学会概念”而是能在本机调用模型服务。Day 2 的验收标准是模型能根据自然语言生成结构化工具参数。Day 3 的验收标准是 Agent 可以通过 Elasticsearch REST API 分析日志。Day 4 迁移框架时不要重写全部代码保留工具层替换调度层。Day 5 生产化时重点看日志、限流、超时和权限控制。7.3 初学者最容易踩的坑第一不打印消息调试靠猜。模型返回的工具调用内容是最重要的调试信息先打印messages再看结果。第二把工具结果直接返回为字典没有转成字符串。很多模型服务要求工具结果必须是文本直接传入字典会导致协议错误。第三工具 Schema 里的required字段缺失。模型可能少传参数工具函数执行时直接报错。第四没有设置max_steps。Agent 会不断调用工具直到请求超时或资金被消耗。第五盲目追求框架。第一天就引入 LangChain出现问题后不知道是框架问题还是模型问题。建议先手写一遍再使用框架。8. 常见问题排查从现象倒推根因8.1 排查顺序Agent 出问题时不要直接改代码按以下顺序排查确认模型能否直接回答普通问题排除模型服务本身故障。确认工具函数能独立运行排除工具代码问题。确认工具 Schema 是否正确模型是否能看到工具描述。打印完整messages确认工具结果是否正确回传。确认上下文是否过长工具结果是否需要截断。8.2 问题与处理表问题现象可能原因检查方式处理建议模型返回空字符串工具结果过大、超时查看请求日志和结果大小截断工具结果增加超时工具参数解析失败Schema 不完整打印tool_calls严格定义required和参数类型Agent 无限循环没设置最大步数查看循环次数增加max_steps工具结果没有生效roletool消息顺序错误打印messages确保结果通过tool_call_id回传ES 查询无数据时间字段名或时区不一致先用 curl 单独测试统一字段名指定时区8.3 ES 查询时间范围踩坑使用now-30m时要求日志时间字段必须是 ES 可识别的日期格式并且索引里确实有近
返回列表