ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

大模型Agent开发实战:从最小闭环到工程化避坑指南

大模型Agent开发实战:从最小闭环到工程化避坑指南

简介:《大模型应用开发 动手做AI Agent》是黄佳撰写的一本面向Agent技术学习者与开发者的实战型电子书,适合研究人员、开发人员、产品经理、企业负责人及高校相关专业师生阅读,帮助读者从技术框架、开发工具到实操项目系统掌握Agent的设计与实现。全书围绕GPT-4、OpenAI Assistants API、LangChain、LlamaIndex、MetaGPT等前沿技术展开,通过7个功能强大的Agent案例,覆盖办公自动化、函数调用、ReAct推理、智能调度、检索增强生成(RAG)以及多Agent框架等方向,并展望Agent的未来趋势。资源包为1个PDF文件,大小约25.35MB,内容完整、结构清晰,便于按章节检索学习。目前已有994人学习下载,读者可借此零距离接触尖端AI技术,理解Agent访问工具、与数据交互及对话完成任务的关键思路,为实际开发提供可落地的参考。

1. 从一份 PDF 说起:大模型 Agent 到底能不能照着做出来

很多人第一次接触 Agent,是在各种演示视频里看它自动查资料、调工具、写代码,感觉像魔法。但真到自己动手,往往卡在第一步:不知道一个 Agent 的最小闭环长什么样,不知道该先写提示词还是先搭框架,更不知道哪些环节是玄学、哪些是硬工程。这份《大模型应用开发 动手做 AI Agent》的 PDF 就是冲着这个痛点来的——它不讲空泛的行业趋势,而是把 Agent 拆成可执行的模块:模型调用、工具注册、任务规划、记忆管理、执行循环。适合两类人:一是刚转到大模型应用方向的开发者,想用最短路径跑通第一个 Agent;二是已经会用 API 但项目总是“演示能跑、上线就崩”的熟手,需要补上工程化那一段。下面我按自己拆包复现的顺序,把这份资料里的关键路径和踩坑点摊开讲。

2. 动手前的环境底座:模型接口、依赖与最小可运行单元

2.1 为什么先锁接口层,而不是先挑框架

Agent 框架换得比手机壳还勤,但底层模型接口的调用方式基本稳定。这份 PDF 里反复强调一个原则:先把模型调用封装成一个独立函数,再往上叠 Agent 逻辑。这样做的好处是,后面无论换模型、换框架,还是加缓存、加重试,都只改一个地方。常见做法是用 OpenAI 兼容接口作为统一入口,因为大多数国内模型服务也支持这套协议。我一般会建一个llm_client.py,把 base_url、api_key、model_name 三个参数抽成环境变量,代码里不出现硬编码。

# llm_client.py import os from openai import OpenAI # 从环境变量读取,避免密钥写进代码库 client = OpenAI( base_url=os.getenv("LLM_BASE_URL"), # 例如 https://api.example.com/v1 api_key=os.getenv("LLM_API_KEY"), ) def chat(messages, model=None, temperature=0.3): """ messages: [{"role": "user", "content": "..."}] temperature: Agent 任务建议 0.2~0.4,太低会死板,太高会乱调工具 """ resp = client.chat.completions.create( model=model or os.getenv("LLM_MODEL_NAME"), messages=messages, temperature=temperature, ) return resp.choices[0].message.content

这段代码的逻辑说明:把客户端初始化放在模块级,避免每次调用都重建连接;chat函数只暴露 messages 和 temperature,模型名走环境变量,方便在测试和生产之间切换。参数上,temperature 对 Agent 特别关键——规划类任务建议 0.2 到 0.4,工具参数生成建议 0.1 到 0.2,创意类子任务可以放到 0.7,但不要全局用一个值。

2.2 依赖装多少算够:一份不臃肿的 requirements

PDF 里没有堆一大堆库,核心依赖其实就几个。我按最小可运行原则整理了一份清单,装完能跑通后面所有示例。

# requirements.txt openai>=1.30.0 # 模型调用 pydantic>=2.0 # 工具参数校验,Agent 调工具翻车多半是参数没校验 tenacity>=8.2 # 重试,网络抖动和限流必备 python-dotenv>=1.0 # 读 .env rich>=13.0 # 终端输出好看点,调试时能看清执行链

安装命令就一行pip install -r requirements.txt。这里有个血泪经验:不要一上来就装 LangChain 全家桶。PDF 里的示例是让你先手写一遍 ReAct 循环,理解每一步在干什么,等手写跑通了再决定要不要上框架。否则出了问题你连是框架的锅还是模型的锅都分不清。

2.3 最小 Agent 循环:手写一遍比看十篇原理强

所谓 Agent,最小闭环就是“想一步、做一步、看结果、再想”。下面这个 30 行的循环,是整份资料里最值得抄作业的部分。

import json from llm_client import chat # 工具注册表:名字 -> 函数 TOOLS = { "get_time": lambda: "2025-01-01 12:00:00", "calc": lambda expr: str(eval(expr)), # 仅演示,生产环境禁用 eval } SYSTEM_PROMPT = """你是一个 Agent。可用工具:{tools} 输出格式必须是 JSON:{{"thought": "...", "action": "工具名或null", "action_input": "参数", "final_answer": "null或答案"}} """ def run_agent(user_input, max_steps=5): messages = [ {"role": "system", "content": SYSTEM_PROMPT.format(tools=list(TOOLS.keys()))}, {"role": "user", "content": user_input}, ] for step in range(max_steps): raw = chat(messages, temperature=0.2) try: decision = json.loads(raw) except json.JSONDecodeError: # 模型偶尔会带 markdown 代码块,这里做一次清洗 raw = raw.strip().strip("```json").strip("```") decision = json.loads(raw) if decision.get("final_answer") not in (None, "null"): return decision["final_answer"] action = decision.get("action") if action in TOOLS: observation = TOOLS[action](decision.get("action_input", "")) else: observation = f"未知工具:{action}" # 把观察结果塞回对话,进入下一轮 messages.append({"role": "assistant", "content": raw}) messages.append({"role": "user", "content": f"观察结果:{observation}"}) return "达到最大步数,任务未完成"

逻辑说明:每一轮让模型输出结构化 JSON,包含思考、动作、参数和最终答案。如果 action 是 null 且有 final_answer,就结束;否则执行工具,把结果作为“观察”拼回对话。参数上,max_steps是保命参数,防止模型陷入死循环烧 token,一般设 5 到 8。temperature 压到 0.2,是为了让 JSON 格式更稳定。这段代码跑通,你就理解了 Agent 的骨架,后面加记忆、加多工具、加规划都是在这个循环上长出来的。

3. 工具调用与任务规划:让 Agent 从“会说”到“会做”

3.1 工具描述怎么写,模型才不调错

工具调用的准确率,八成取决于描述,而不是模型能力。PDF 里给了一个很实用的模板:工具名用动词开头,描述里写清楚“什么时候用、输入是什么、输出是什么、失败会怎样”。我对比过,同一套工具,描述写详细后调用准确率能从六成提到九成。

TOOL_SCHEMA = { "name": "search_docs", "description": "在公司内部文档库中搜索关键词。当用户询问产品参数、流程规范时使用。输入是搜索词字符串,返回最相关的前三条摘要。如果无结果返回空字符串。", "parameters": { "type": "object", "properties": { "query": {"type": "string", "description": "搜索关键词,不要带问句"} }, "required": ["query"], }, }

参数说明:description里明确“什么时候用”比“这个工具能干什么”更重要,因为模型是在做选择,不是在做阅读理解。query的描述里加一句“不要带问句”,能减少模型把整句用户问题直接塞进去的情况。常见做法是给每个工具写三到五条正例和反例,放在系统提示里,效果比调 temperature 明显。

3.2 任务规划:先拆步骤,再决定用哪个工具

复杂任务直接丢给 Agent,它容易东一榔头西一棒子。PDF 里推荐的做法是加一个“规划器”角色,先让模型输出步骤列表,再逐步执行。下面是一个规划提示的骨架。

PLANNER_PROMPT = """把用户任务拆成 2~5 个步骤,每步只做一件事。 输出 JSON 数组:[{"step": 1, "goal": "...", "tool_hint": "可能用到的工具名或null"}] 不要输出解释。""" def plan(task): raw = chat([ {"role": "system", "content": PLANNER_PROMPT}, {"role": "user", "content": task}, ], temperature=0.3) return json.loads(raw)

逻辑说明:规划器只负责拆步骤,不负责执行,这样职责单一,出错好定位。tool_hint是给执行器的建议,不是强制,执行器仍然可以自己选工具。参数上,步骤数限制在 2 到 5,太多会引入不必要的中间环节,太少又起不到规划作用。我一般会在规划后加一步人工确认(调试阶段),把步骤打印出来看一眼,确认没跑偏再往下走。

3.3 记忆管理:短期靠对话,长期靠摘要

Agent 跑多轮之后,对话历史会越来越长,token 成本飙升,模型还会“忘记”前面的关键信息。PDF 里给了一个折中方案:保留最近 N 轮原始对话,更早的内容压缩成摘要。实现上就是一个滑动窗口加一个摘要函数。

def compress_history(messages, keep_recent=6): if len(messages) <= keep_recent: return messages old = messages[:-keep_recent] recent = messages[-keep_recent:] summary = chat([ {"role": "system", "content": "把以下对话压缩成三句话以内的摘要,保留关键事实和结论。"}, {"role": "user", "content": json.dumps(old, ensure_ascii=False)}, ], temperature=0.2) return [{"role": "system", "content": f"历史摘要:{summary}"}] + recent

参数说明:keep_recent控制保留多少条原始消息,一般设 4 到 8,取决于单条消息长度。摘要用低 temperature,保证事实不跑偏。注意摘要会丢细节,所以关键结论最好在生成时就写进一个独立的“事实表”,而不是只靠对话历史。

4. 避坑与排查:那些演示看不出来的翻车点

4.1 模型返回的 JSON 带 markdown 代码块

现象:json.loads报JSONDecodeError,打印出来发现内容被json 和包着。原因:模型在训练时见过大量 markdown 格式的代码,习惯性加围栏。解决:在解析前做一次清洗,去掉首尾的代码块标记;更稳的做法是在系统提示里明确“只输出 JSON,不要 markdown”,双管齐下。

4.2 工具参数类型对不上

现象:工具函数收到的是字符串"3",但内部逻辑需要整数3,导致计算或查询出错。原因:模型输出的 JSON 里数字经常带引号。解决:用 pydantic 做一层参数校验和类型转换,在工具注册时定义好 schema,调用前先model_validate,类型不对就返回错误信息让模型重试。

4.3 循环停不下来,token 烧得心疼

现象:Agent 反复调用同一个工具,或者一直在“思考”不输出最终答案。原因:没有设置最大步数,或者工具返回的空结果让模型以为没执行成功。解决:强制max_steps,并在工具返回空时明确告诉模型“本次调用无结果,请换策略或给出最终答案”,而不是返回空字符串让它猜。

4.4 换了模型之后全部崩盘

现象:同一套提示词,A 模型跑得好好的,换 B 模型后 JSON 格式全乱。原因:不同模型对格式指令的遵循程度差异很大,尤其是小参数模型。解决:把格式约束从系统提示里再复制一份到用户消息末尾,形成“双重强调”;同时准备一个降级解析器,用正则提取关键字段,而不是直接json.loads。

4.5 工具描述里的“隐藏假设”

现象:模型调用了正确的工具,但传的参数是它自己编的,不是用户给的。原因:工具描述里没写清楚参数来源,模型就“合理想象”了一个。解决:在描述里加一句“参数必须来自用户输入或上一步观察结果,不得自行编造”,并在执行前做一次来源校验,对不上就拒绝执行并让模型重新生成。

5. 进阶技巧:把 Agent 从能跑变成好用

5.1 用“执行轨迹”做回归测试

Agent 的输出不稳定,今天能跑通不代表明天还能。我的习惯是每次调试成功后,把完整的执行轨迹(每轮的 thought、action、observation)存成 JSON 文件,作为回归用例。下次改提示词或换模型,先跑一遍这些用例,看轨迹是否还在预期路径上。这比只看最终答案靠谱得多,因为最终答案对了,过程可能已经跑偏,只是运气好。

import json, hashlib def save_trace(task, trace): key = hashlib.md5(task.encode()).hexdigest()[:8] with open(f"traces/{key}.json", "w", encoding="utf-8") as f: json.dump({"task": task, "trace": trace}, f, ensure_ascii=False, indent=2)

参数说明:用任务文本的哈希做文件名,避免重复;trace 里记录每一轮的完整消息,方便 diff。我一般会攒 20 到 30 条覆盖不同工具和边界情况的用例,改一次提示词就跑一遍,几分钟的事,能省下大量线上排查时间。

5.2 给工具加“干跑”模式

生产环境最怕 Agent 误调写操作。我的做法是给每个工具有一个dry_run参数,默认 True,只返回“将会执行什么”,不真正执行。调试阶段全用干跑,确认轨迹没问题再关掉。这个习惯是从一次误删测试数据之后养成的,从那以后我每次接新工具都强制走一遍干跑验证。

模式行为适用阶段
dry_run=True只打印将要调用的工具和参数调试、回归测试
dry_run=False真实执行并返回结果确认后的生产调用

5.3 提示词版本管理:别再用“最终版2”命名了

Agent 项目里提示词就是核心代码,但很多人把它散在 py 文件里,改一次覆盖一次,出了问题回不去。我的做法是把所有提示词抽到一个prompts/目录,每个文件带版本号,用 git 管理。改提示词走正常的代码评审流程,而不是随手改。这样当 Agent 行为变化时,能快速定位是哪次提示词改动引入的。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进