ARTICLE DETAIL

资讯详情

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

从零构建AI Agent:ReAct架构、工具调用与工程落地全指南

从零构建AI Agent:ReAct架构、工具调用与工程落地全指南 简介资源聚焦人工智能与AI Agent智能体开发面向具备一定AI/机器学习基础的研发人员、产品经理和技术爱好者内容从基础概念延伸到项目实战系统梳理了机器学习、深度学习、大语言模型、AIGC与提示工程等知识并深入讲解AI Agent与传统程序的区别及在自媒体、智能客服、自动驾驶等场景的应用。资源以字节跳动扣子COZE平台为主线完整拆解需求梳理、软件选型、提示工程、数据库搭建、UI界面、测试评估、部署发布七个步骤帮助读者从零搭建自己的智能体。其中包含抖音短视频文案转小红书笔记、小红书文案OCR飞书同步两个实战案例覆盖内容创作与数据处理场景附有操作流程和代码示例便于边学边练。资源为单个PDF文档大小12.01MB内容结构紧凑、理论与实践并重目前已有1356人学习适合希望快速上手AI Agent开发与落地的读者。1. 为什么“写提示词”和“开发Agent”之间隔着一整条工程链几年前我第一次让AI自动完成多步骤任务第一反应是把所有步骤写进提示词。结果每次都在同一个环节翻车字段丢了、格式飘了、中途卡住没人管。那不是提示词功底的问题而是方案错了。AI Agent和普通对话式AI的根本区别在于模型被放进一个循环里先生成推理再决定调用什么工具把工具返回结果放回上下文接着做下一步决策。这个“推理→行动→观察”的闭环才是智能体真正的内核。这个标题覆盖的是一条从0到1的完整链路Agent的基础概念与主流架构、搭建最小可用系统、接入检索和外部服务、部署后的高频坑、交付前的评估手段。适合两类人有Python基础、想把Agent应用到自己项目或产品里的开发者被各类智能体框架的包装吸引、想看清底层循环逻辑的从业者。人工智能正从尝鲜工具变成日常帮手但真正能交付价值的不只是模型本身而是围绕它搭起来的这一套工程循环。2. AI Agent能“自己干活”的前提核心架构与主流实现路径2.1 ReAct范式把“想一步做一步”翻译成代码循环ReAct来自Reason Act两个词的组合。它的核心思想是大模型在每个决策点先输出一段推理再基于推理选择一个动作去执行而不是憋一个大计划然后一次性做完。和普通对话相比ReAct多了一层结构思考与行动交替出现每轮都留下可追溯的轨迹。state load_system_prompt() while not finished(state): thought llm.generate(state) # 推理根据当前状态思考下一步 action, args parse(thought) # 解析从输出里拆出动作和参数 result execute(action, args) # 执行调用工具或外部服务 state.append(result) # 观察把结果写回上下文这段骨架是几乎所有Agent实现的地基。finished(state)是终止判断可以是任务完成标志也可以是最大步数。parse(thought)这一步最容易被忽略模型输出的是自然语言你必须用强规则把它转成结构化动作否则后面没法可靠执行。为什么这个范式能落地因为模型每轮只需要做一个小决策而不是远程规划一整条路径。工具返回的真实结果会作为新证据放回上下文后续推理就有了依据幻觉率明显低于“一步到位”的生成方式。工程上常见的做法是强制模型输出JSON里面带thought、action、args三个字段让解析逻辑简单且可校验。2.2 记忆分层上下文窗口和真正要落库的记忆是两码事很多刚上手的人把上下文窗口当成记忆这是一个成本极高的误解。上下文窗口是短期工作记忆每次请求都要把所有历史重新发给模型token费用随轮数线性上涨。真正能被反复使用的记忆应该分层管理。第一层是会话记忆也就是最近几轮对话历史通常保留最近10到20条消息即可。第二层是工作记忆指当前任务运行中的中间结果比如正在处理的订单号、已经查到的库存数量放在内存里的变量或Redis这类缓存中。第三层才是长期记忆是需要跨会话复用的知识比如用户偏好、业务规则、文档知识点这些必须落到数据库或向量库。判断要不要上向量库我一般用一条很简单的标准如果单轮任务需要带入的参考内容已经超过上下文窗口的一半就该考虑外置检索而不是换更大的窗口硬扛。几十条固定规则直接塞system prompt效果更好、成本更低、排查也更容易。Agent开发里最常见的架构错误就是什么记忆都往上下文里塞最后上下文爆炸、响应变慢、费用失控。2.3 工具调用落地Function Calling、代码直调与MCP模型怎么知道该调哪个工具目前有三种主流落地方式。第一种是Function Calling模型在返回结构里自带tool_calls字段包含工具名和参数SDK负责解析。这种方式适合参数固定、Schema明确的工具比如查天气、发邮件、下单。第二种是代码直调模型只输出一个动作名称由我们自己的代码去匹配和分派。这种方式更灵活适合工具数量少、动作高度定制化的场景也更容易调试。第三种是MCP协议把工具定义和调用方式协议化工具变成可动态发现的资源。它确实解决了工具生态互通的问题但协议本身还在演进接口变动频繁如果不是团队已经定了技术栈没必要为了追新而上。方式适用场景优点典型坑Function Calling参数固定的标准工具模型原生支持解析可靠不同供应商实现细节不一致代码直调少量定制化动作调试直观依赖少工具一多分派逻辑变重MCP多系统工具互联动态发现生态互通接口不稳定学习成本高我的建议是项目初期先用代码直调把循环跑通工具数量上来了再考虑Function Calling。直接上MCP会把排查问题的范围扩大对从0开始的Agent项目不是最优路径。2.4 主流智能体框架速览LangGraph、AutoGen与自研循环的取舍目前说到Agent框架绕不开LangGraph、AutoGen和自研循环这三条路线。LangGraph把节点和边显式建模适合复杂工作流但抽象层多出问题时排查链路太长。AutoGen偏多Agent协作研究适合做实验离生产环境还有距离。自研循环指我们自己写ReAct骨架代码量不大但每一步都在掌控之内。路线核心优势主要代价适合阶段LangGraph可视化编排状态管理完善学习曲线陡版本升级容易破坏行为复杂工作流、团队协作AutoGen多Agent会话灵活生产化案例少不确定性强研究原型、方案探索自研ReAct完全可控依赖最少需要自己处理解析和边界产品落地、调用路径固定我一般会把框架当参考实现核心路径自己写。原因不是框架不好而是Agent产品的瓶颈往往在工具行为、上下文管理和错误处理上这些恰恰是框架难以替你定制的部分。等自研循环稳定了再按需引入周边能力比一开始就套一个全家桶要省心得多。3. 从0搭建最小可用Agent项目结构、核心循环与参数调优3.1 先建项目依赖、环境变量与目录约定这一步的目标是把环境收拾干净。不要全局装包不要硬编码模型名和API Key所有能变的东西一律走环境变量。mkdir my-agent cd my-agent python -m venv .venv source .venv/bin/activate pip install openai python-dotenv touch agent.py tools.py rag.py .envopenai这个包在这里只充当统一的模型调用SDK只要你的接口兼容OpenAI风格base_url指向对应服务即可。python-dotenv用来加载.env文件里的配置。.env里至少要有三样东西API Key、模型名、接口地址。LLM_API_KEYyour_key_here LLM_BASE_URLhttps://your-endpoint.example.com/v1 LLM_MODELyour-model-name把模型名放进环境变量而不是写死在代码里是因为切换模型是Agent开发里的家常便饭。本地调试用便宜小模型正式跑用能力更强的模型改一行配置就能切不用改代码。.env文件要加入.gitignore避免密钥泄漏。3.2 ReAct最小循环模型调用、动作解析与工具分派现在写真正的ReAct循环。这个文件是整个Agent的心脏后续所有工具、记忆、检索都挂在这条循环上。# agent.py — 一个不依赖框架的ReAct最小实现 import json, os from openai import OpenAI from dotenv import load_dotenv load_dotenv() client OpenAI(api_keyos.getenv(LLM_API_KEY), base_urlos.getenv(LLM_BASE_URL)) SYSTEM_PROMPT 你是一个AI Agent。每轮必须输出JSON格式为 {thought: 分析当前情况, action: 工具名或none, args: {参数名: 参数值}} 当任务完成时将action设为finish并在args里给出最终答案。 def run_agent(task, tools, max_steps10): messages [{role: system, content: SYSTEM_PROMPT}, {role: user, content: task}] for step in range(max_steps): resp client.chat.completions.create( modelos.getenv(LLM_MODEL), messagesmessages, temperature0, ) content resp.choices[0].message.content parsed try_parse_json(content) # 容错解析见下文 if parsed is None: messages.append({role: user, content: 输出不是合法JSON请重新输出。}) continue if parsed[action] finish: return parsed[args][answer] tool tools.get(parsed[action]) if tool is None: messages.append({role: user, content: f工具 {parsed[action]} 不存在请换一个已注册的工具。}) continue observation tool(**parsed.get(args, {})) messages.append({role: assistant, content: content}) messages.append({role: user, content: f工具返回{observation}}) raise TimeoutError(f{max_steps} 步内未完成任务)这段代码的核心逻辑有四点。第一模型输出后必须经过容错解析直接json.loads会死在各种意外格式上。第二tool从tools字典里按名字取取不到就告诉模型换工具而不是直接崩溃。第三每轮都把模型原始输出和工具观察结果分别追加进消息列表模型下一轮才能基于真实结果继续推理。第四max_steps10是硬止损防止模型陷入无限循环。两个关键参数值得单独说。temperature0是Agent项目的基本设置业务逻辑需要确定性不需要创造性温度越高越容易出现同一个问题两次跑出不同结果。max_steps的默认值要看任务复杂度简单查证任务5步够用多工具协作任务建议10到15步再大就要考虑是不是任务拆解有问题。3.3 用装饰器做工具注册让Agent运行期动态发现能力工具不能散落在if-else里否则工具一多代码就变成一坨。用装饰器注册是干净且可扩展的做法。# tools.py — 用装饰器把函数注册成Agent可调用的工具 TOOL_REGISTRY {} def tool(nameNone): def decorator(func): registry_name name or func.__name__ TOOL_REGISTRY[registry_name] func return func return decorator tool(calculator) def calculator(expression: str) - str: 安全计算表达式只允许数字、运算符和括号 allowed set(0123456789-*/(). ) if not set(expression).issubset(allowed): return error: 表达式包含非法字符 try: return str(eval(expression, {__builtins__: {}}, {})) except Exception as e: return ferror: {e}装饰器把函数和名字注册进TOOL_REGISTRY主循环里只要把这个字典传进去就能用。calculator用白名单过滤了字符比直接eval安全得多。注意这里用eval是为了演示生产环境应该用ast模块解析表达式树再计算风险更低。注册机制的好处是新增工具只需要写一个普通函数加一行装饰器不需要改动主循环代码。Agent项目跑起来之后工具会越来越多能不能快速往上挂新能力直接决定了这个系统的迭代速度。3.4 三个必调参数temperature、max_steps与终止条件这三个参数是Agent跑起来之后最先要调的它们的默认值可以照抄但上线前必须根据自己的业务重新校准。参数建议默认值作用调整方向temperature0控制输出随机性业务要求确定性就保持0头脑风暴场景才调高max_steps10控制最大循环轮数任务链路长就调大成本敏感就调小终止条件actionfinish定义任务完成标准一定要校验args里有没有最终答案防止空结果退出temperature调高的唯一理由是你真的需要模型“发挥”。Agent场景里工具调用的参数解析错一个字符就会传导到后面所有步骤所以确定性比创造性值钱得多。max_steps太小会让复杂任务提前失败太大会让单次运行成本失控配合终止条件里的答案校验才能保证任务“完成”而不是“假装完成”。4. 给Agent接入真实能力RAG检索、代码执行与外部API4.1 接私有知识库切分、检索与上下文组装的三个节点Agent能调用工具只是第一步真正让它“懂业务”的是能读到私有知识。RAG链路有三个关键节点每个节点都有坑。节点一是文档切分。常见做法是按固定长度切块比如每512个字符切一块相邻块之间重叠64个字符。固定切分的问题是会拦腰切断一个完整段落检索时上下文语义缺失。我一般优先按段落结构切段落太长的再按句子边界补切。节点二是向量检索。把切好的文档块做embedding入库用户提问时也做embedding然后算余弦相似度取TopK。代码实现如下。# rag.py — 最小RAG检索embedding 余弦相似度 import os import numpy as np from openai import OpenAI load_dotenv() client OpenAI(api_keyos.getenv(LLM_API_KEY), base_urlos.getenv(LLM_BASE_URL)) def embed(text: str): resp client.embeddings.create( modelos.getenv(EMBED_MODEL, text-embedding-3-small), inputtext) return np.array(resp.data[0].embedding) def search(question, chunks, top_k3): q_vec embed(question) scored [] for idx, chunk in enumerate(chunks): vec chunk[vector] score np.dot(q_vec, vec) / (np.linalg.norm(q_vec) * np.linalg.norm(vec) 1e-9) scored.append((score, idx)) scored.sort(keylambda x: x[0], reverseTrue) return [chunks[i] for _, i in scored[:top_k]]top_k3是起步值。文档质量高、切块规整时3到5块足够文档噪声大时提高top_k反而会把不相关内容带进上下文。节点三是上下文组装把检索到的内容拼进system prompt时要明确告诉模型“以下内容来自知识库可能不包含答案不要编造”能有效压住幻觉。4.2 让Agent能执行代码子进程沙箱与安全边界Agent学会执行代码之后能力会上一个台阶但风险也同步上升。模型生成的代码不能直接在当前进程里exec那等于把整个服务的安全交给了模型。常见做法是把代码丢给子进程执行并做时间和资源限制。# exec_tool.py — 在子进程里执行模型生成的Python代码 import subprocess, tempfile, os def run_python_code(code: str, timeout_seconds: int 5) - str: # 阻断明显危险的系统调用 blocked [import os, import sys, subprocess, socket, requests] if any(b in code for b in blocked): return error: 该操作被沙箱禁止 try: proc subprocess.run( [python, -I, -c, code], capture_outputTrue, textTrue, timeouttimeout_seconds) return proc.stdout or proc.stderr except subprocess.TimeoutExpired: return error: 执行超时python -I参数会忽略当前环境变量和用户级site-packages相当于在隔离环境里跑比裸跑安全一截。timeout_seconds5防止模型生成死循环代码卡住整个Agent。这段代码只是第一道防线真正的安全边界是容器隔离用Docker把进程网络、文件系统都隔离掉才是生产级做法。timeout_seconds要按任务调简单计算1秒足够数据清洗任务可以放宽到10秒再长就该怀疑代码质量了。子进程沙箱的一个隐藏坑是执行环境要跟开发环境对齐模型生成的代码大概率调用你常用的库沙箱里没装就是报错所以沙箱镜像要尽量贴近本地环境。4.3 接外部服务鉴权、超时、重试与限流的工程细节Agent接外部API最典型的坑是超时无感知。模型调一个HTTP接口接口没响应整个循环卡在那里白白耗token。必须给每个外部调用加超时和重试。# api_tool.py — 给外部HTTP调用加超时与指数退避重试 import time import requests def call_with_retry(url, headersNone, payloadNone, timeout10, max_retries3): for attempt in range(max_retries): try: resp requests.post(url, headersheaders, jsonpayload, timeouttimeout) resp.raise_for_status() return resp.json() except requests.Timeout: time.sleep(2 ** attempt) # 指数退避 except requests.RequestException as e: if attempt max_retries - 1: return {error: str(e)} time.sleep(2 ** attempt) return {error: reached max retries}timeout10是给单次请求的硬上限不设置的话默认是永久等待这在Agent里是致命的。max_retries3配合指数退避重试间隔分别是1秒、2秒、4秒给下游服务留恢复时间。要注意只有网络错误和超时才值得重试业务错误码如404、400重试多少次都没用直接返回错误给模型让它换一种方式。限流是AI Agent部署后一定会遇到的事。上游API对并发有限制Agent一接入真实用户流量就出现大量429。简单做法是在工具层加一个令牌桶每秒只放行N个请求超出的排队等待。这个N先用保守值观察一段时间再调大比一开始拼命压上限然后被限流封禁要稳。5. Agent开发与部署避坑6个高频问题的排查路径5.1 死循环、JSON解析这类“结构性”故障坑一Agent陷入死循环反复调用同一个工具直到撞上max_steps才报错退出。现象日志里连续10轮以上出现相同的action工具参数都没变。原因终止条件只依赖轮数上限没有检测“重复动作”这个信号。解决在循环里维护一个最近动作队列连续出现3次完全相同的(action, args)就主动中断并把“你已经重复调用请换一个方法”作为新消息塞回上下文。坑二模型输出的JSON解析失败整个循环直接崩溃。现象json.loads抛出Expecting value异常查日志发现模型输出被markdown代码块包裹或者在JSON里夹带了注释。原因每轮都让模型输出JSON但没做容错处理运气不好就遇上一次格式污染。解决不要直接用json.loads先剥离代码块再提取花括号内容解析失败时把错误提示回灌给模型让它重新输出。# 容错解析先剥离markdown代码块再尝试提取JSON import json, re def try_parse_json(content: str) - dict | None: cleaned re.sub(r(?:json)?, , content).strip() try: return json.loads(cleaned) except json.JSONDecodeError: match re.search(r\{.*\}, cleaned, re.DOTALL) if match: try: return json.loads(match.group(0)) except json.JSONDecodeError: return None return None这段try_parse_json是处理“模型不听话”的第一道保险。正则剥离代码块再用花括号匹配兜底。仍然失败就返回None主循环收到None会提示模型重新输出而不是让整个任务崩掉。解析容错是Agent开发里投入产出比最高的一段代码几乎所有线上事故都能在这里被拦下一半。5.2 上下文爆炸与并发限流成本类的两个坑坑三任务跑到第20轮时单次请求的token数量已经是前几轮的几倍费用肉眼可见地失控。现象日志里prompt_tokens一路走高响应时间也在变慢。原因每一轮都把完整历史塞进上下文会话越长开销越大而且大部分历史已经是无用信息。解决给历史消息设置窗口上限比如只保留最近15条更早的消息做摘要压缩用一个“总结已完成的步骤”追加到上下文中。这个方案在保留上下文连贯性的同时把token消耗控制在一个可接受的范围内。坑四Agent部署上线后一接入真实请求就大量报429或限流错误。现象单机压测正常流量稍微一起来上游API就开始拒绝服务。原因没有做并发控制多个Agent实例同时无节制地调用同一个外部服务。解决在工具调用层加一个限流器按上游配额设定每秒最大请求数超出部分排队等待。# rate_limit.py — 简单的令牌桶限流 import time, threading class TokenBucket: def __init__(self, rate: float, capacity: int): self.rate rate # 每秒补充的令牌数 self.capacity capacity # 桶的最大容量 self.tokens capacity self.updated time.monotonic() self.lock threading.Lock() def acquire(self, tokens1): with self.lock: now time.monotonic() self.tokens min(self.capacity, self.tokens (now - self.updated) * self.rate) self.updated now if self.tokens tokens: self.tokens - tokens return True return Falserate和capacity的取值先保守后收紧比如上游配额是100次/分钟就把rate设成1.5capacity设成2留出缓冲。限流器加在工具函数入口处拿不到令牌就等待或返回错误宁可工具失败也不能把上游打挂。5.3 环境不一致与输出不确定部署阶段的两个坑坑五本地跑得好好的部署到服务器就玄学报错模型名找不到、接口地址重复拼接、API Key为空。现象同样的代码本地验证通过服务器上第一次调用就报401或404。原因.env文件没有同步到部署环境或者部署平台的环境变量没配置全代码里硬编码的配置和服务器上的配置不一致。解决把全部可变配置集中到.env写一份.env.example提交到代码仓库部署时从模板复制并填入真实值。部署后第一件事是用一个只请求一次的最小脚本验证配置项是否全部加载成功而不是直接跑完整任务。坑六同一个问题跑两次答案不一样被团队当成Bug反复排查。现象回归测试时同一个Prompt得到不同结果定位半天发现不是代码问题。原因temperature没有固定或者没有设置随机种子模型本身存在随机性。解决Agent项目里temperature固定为0如果供应商支持还设置seed参数同时保存每次运行的完整轨迹把模型输出的差异和配置差异对应起来而不是凭感觉判断。输出不确定性是Agent项目的常态能做的不是消灭随机性而是用配置和日志把它约束到可解释的范围内。6. 把Agent的黑匣子打开轨迹日志、回归集与一个调试习惯Agent开发和传统后端开发最大的不同是你没法靠断点调试因为问题可能出在模型第8轮的某一句推理里。交付前我习惯做三件事。第一件事是给每次运行写完整轨迹日志。每一轮的模型原始输出、解析结果、工具名、工具参数、工具返回、累计token数全部落盘成一份JSON。排查问题的时候直接看轨迹文件一眼就能看出模型在哪一步开始跑偏。没有这份日志Agent就是一个彻头彻尾的黑匣子出了问题只能靠猜。第二件事是维护一个回归场景集。把平时压测Agent的场景比如“查询订单状态并计算总价”“从文档中提取指定字段并生成表格”这类典型任务收集成固定列表。每次改完工具或调整Prompt用同一批任务跑一遍对比结果差异。我把这个习惯看作Agent版的“智能体面试”题库模型换版本、代码重构、Prompt调整都靠它来做底线保障。第三件事是我个人的调试习惯先固定温度到0再跑三次。如果三次结果一致说明逻辑链路确定可以继续下一步如果结果飘就先查配置里temperature是不是被改过再查Prompt里是不是用了模棱两可的表述。等结果稳定了再考虑微小调高温度换取更灵活的推理。我自己翻车最惨的一次是上线前把所有temperature从0改到0.8想着让回答更“自然”结果工具参数解析错误率直接翻倍一个订单查询任务跑了11轮还在循环。从那以后我立了个规矩生产环境跑Agent随机性永远是敌人不是朋友。把这个调试习惯带进你的项目能少踩非常多的坑。希望帮到你。本文还有配套的精品资源点击获取
返回列表