☰
从零构建AI Agent:ReAct架构、工具调用与工程落地全指南
2026/10/8 3:09:49 网站建设 项目流程

简介:资源聚焦人工智能与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 .env

openai这个包在这里只充当统一的模型调用SDK,只要你的接口兼容OpenAI风格,base_url指向对应服务即可。python-dotenv用来加载.env文件里的配置。.env里至少要有三样东西:API Key、模型名、接口地址。

LLM_API_KEY=your_key_here LLM_BASE_URL=https://your-endpoint.example.com/v1 LLM_MODEL=your-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_key=os.getenv("LLM_API_KEY"), base_url=os.getenv("LLM_BASE_URL")) SYSTEM_PROMPT = """你是一个AI Agent。每轮必须输出JSON,格式为: {"thought": "分析当前情况", "action": "工具名或none", "args": {"参数名": "参数值"}} 当任务完成时,将action设为"finish",并在args里给出最终答案。""" def run_agent(task, tools, max_steps=10): messages = [{"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": task}] for step in range(max_steps): resp = client.chat.completions.create( model=os.getenv("LLM_MODEL"), messages=messages, temperature=0, ) 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_steps=10是硬止损,防止模型陷入无限循环。

两个关键参数值得单独说。temperature=0是Agent项目的基本设置,业务逻辑需要确定性,不需要创造性,温度越高越容易出现同一个问题两次跑出不同结果。max_steps的默认值要看任务复杂度,简单查证任务5步够用,多工具协作任务建议10到15步,再大就要考虑是不是任务拆解有问题。

3.3 用装饰器做工具注册:让Agent运行期动态发现能力

工具不能散落在if-else里,否则工具一多代码就变成一坨。用装饰器注册是干净且可扩展的做法。

# tools.py — 用装饰器把函数注册成Agent可调用的工具 TOOL_REGISTRY = {} def tool(name=None): 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 f"error: {e}"

装饰器把函数和名字注册进TOOL_REGISTRY,主循环里只要把这个字典传进去就能用。calculator用白名单过滤了字符,比直接eval安全得多。注意这里用eval是为了演示,生产环境应该用ast模块解析表达式树再计算,风险更低。

注册机制的好处是新增工具只需要写一个普通函数加一行装饰器,不需要改动主循环代码。Agent项目跑起来之后,工具会越来越多,能不能快速往上挂新能力,直接决定了这个系统的迭代速度。

3.4 三个必调参数:temperature、max_steps与终止条件

这三个参数是Agent跑起来之后最先要调的,它们的默认值可以照抄,但上线前必须根据自己的业务重新校准。

参数建议默认值作用调整方向
temperature0控制输出随机性业务要求确定性就保持0,头脑风暴场景才调高
max_steps10控制最大循环轮数任务链路长就调大,成本敏感就调小
终止条件action=finish定义任务完成标准一定要校验args里有没有最终答案,防止空结果退出

temperature调高的唯一理由是你真的需要模型“发挥”。Agent场景里工具调用的参数解析错一个字符就会传导到后面所有步骤,所以确定性比创造性值钱得多。max_steps太小会让复杂任务提前失败,太大会让单次运行成本失控,配合终止条件里的答案校验,才能保证任务“完成”而不是“假装完成”。

4. 给Agent接入真实能力:RAG检索、代码执行与外部API

4.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_key=os.getenv("LLM_API_KEY"), base_url=os.getenv("LLM_BASE_URL")) def embed(text: str): resp = client.embeddings.create( model=os.getenv("EMBED_MODEL", "text-embedding-3-small"), input=text) return np.array(resp.data[0].embedding) def search(question, chunks, top_k=3): 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(key=lambda x: x[0], reverse=True) return [chunks[i] for _, i in scored[:top_k]]

top_k=3是起步值。文档质量高、切块规整时,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_output=True, text=True, timeout=timeout_seconds) return proc.stdout or proc.stderr except subprocess.TimeoutExpired: return "error: 执行超时"

python -I参数会忽略当前环境变量和用户级site-packages,相当于在隔离环境里跑,比裸跑安全一截。timeout_seconds=5防止模型生成死循环代码卡住整个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, headers=None, payload=None, timeout=10, max_retries=3): for attempt in range(max_retries): try: resp = requests.post(url, headers=headers, json=payload, timeout=timeout) 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"}

timeout=10是给单次请求的硬上限,不设置的话默认是永久等待,这在Agent里是致命的。max_retries=3配合指数退避,重试间隔分别是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, tokens=1): 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 False

rate和capacity的取值先保守后收紧,比如上游配额是100次/分钟,就把rate设成1.5,capacity设成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,随机性永远是敌人,不是朋友。把这个调试习惯带进你的项目,能少踩非常多的坑。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询