为了方便写作,本文会把大语言模型、AI、大模型等表述混用,读者只需要知道它们表达的都是 GPT (Generative Pre-trained Transformer, 生成式预训练变换器) ,也就是现在生活常用的、比如 ChatGPT、DeepSeek 等可以对话的那种东西即可。
是案例啊 某天,你对AI说:“帮我写一首诗”,它唰唰唰给你输出了一首看上去还可以的诗。 你又问它:“帮我查一下今天杭州的天气。”它告诉你,今天杭州天气晴朗,温度达到了 30 度,然后你穿着短袖出门,结果被 20 度的天气加上阴冷的风雨送进了医院。
AI 对话 vs Agent 我们要明确的是,大语言模型的输入输出行为本质可以看成基于概率的预测,比如“我想吃”是当前已知的状态,预测下一个状态时,显然“苹果”比“医生”的概率更大。
哦哦哦 —— 也就是说 AI 只能在已知的信息上预测,那么为什么我的 GPT、豆包可以准确查到今天的天气呢?这就引出我们今天要讲的主题了:Agent。
Agent不仅可以帮你查天气,实际上还可以帮你点外卖、帮你炒股、帮你工作等等等,Agent实在是太方便啦。你们搞大模型的简直是码神,你们解放了前端兄弟,还要解放后端兄弟,测试兄弟,运维兄弟,解放网安兄弟,解放ic兄弟,最后解放自己解放全人类!
能做什么 你在周五下午五点半突然增加了一个来自 PM 的需求:“帮我统计一下上个月所有用户的消费总额,按城市分组,然后发到我邮箱。”
在没有Agent的世界线,你默默打开数据库、手写了一个 SQL、然后导出 Excel、打开邮件、粘贴、发送、关机走人。
在有了Agent的世界线:你直接把这句话原封不动地发送给Agent,几分钟后Agent告诉你,它已经帮你完成了这个需求。
让我们想一想在对话窗口的背后,Agent大概做了哪些事:
分析需求,确定要查数据库,将需求转化成 SQL 语句
调用数据库查询工具,执行 SQL
把结果整理成表格,也许还会有些样式
调用邮件发送工具,发给项目经理
回复你:”已发送,您下班吧!”
所以,Agent确实是一个很合适、很形象的名字,直译过来就是“代理”、“助理”,接管我们的部分工作,解放双手!解放大脑!有意思,它是怎么做到的呢?
怎么做到的! 工具调用 工具调用应该是Agent最核心的能力。我们可以通过解析 AI 的格式化输出,判断出需要调用的工具。
简单来说,我们可以构建提示词,提前告诉大模型我这里有哪些工具可以用,大模型在思考过程中,如果判断需要某个工具,就输出一段结构化的调用请求,我们的程序解析出请求后去实际执行,把结果再喂回给大模型。
整个流程大概长这样:
1 2 3 4 5 6 7 8 9 10 sequenceDiagram participant 用户 participant LLM participant 工具 用户->>LLM: "杭州今天天气怎么样?" LLM->>LLM: 我需要查天气,调用 get_weather 工具 LLM->>工具: get_weather(city="杭州") 工具->>LLM: {"temp": 20, "condition": "小雨"} LLM->>用户: "杭州今天20度,小雨,建议多穿衣服"
大模型本身不会真的去发 HTTP 请求,但是我们可以规定它输出“调用什么工具、传什么参数”,然后我们再写好程序去执行这些工具,由这些工具内部发送请求,再将执行结果交给大模型。
大模型有个天然缺陷:训练数据有截止日期,意味着随着时间的推移,大模型无法获得新的知识,当你在 2026 年问ChatGPT今天是多少号时,它能准确回答这个问题并不是因为它的模型每天都在更新,只是因为它拥有一个查询时间的工具。
RAG(检索增强生成) RAG(Retrieval-Augmented Generation)也是一种解决大模型无法获得新的知识的方案,核心思路就是把知识库切片、向量化,存进向量数据库,用户提问时,先去向量数据库检索相关片段,把检索到的内容塞进提示词,让大模型基于这些内容回答。
遗憾的是,本文只打算把进度推到工具调用,这里就不过多介绍了:D。
更多的抛瓦 Agent还有很多有意思的功能,这里点名不展开:
多模态 :处理图片、音频、视频,可以是输入,也可以是输出
记忆系统 :短期记忆(当前对话上下文)+ 长期记忆(跨对话的用户偏好)
SKILL :字面意思,技能就是将多个工具封装在一起
以上的功能其实我们也可以认为是工具调用的一种,因为本质都是调用某些封装好的函数嘛,甚至也可以将RAG也看作是一种工具,既然有了那么多工具、那么多能力,我们要如何保证Agent能正确运用到这些工具呢?换言之,我们需要一个大脑、一个调度任务的指挥😤
大脑 如何让Agent正确运用工具是关键也是难点,我们需要给Agent添加一个聪明的大脑,即设计模式。
设计模式这个专业的用语要怎么理解呢?从字面意思上来看,是一种系统层级的设计方式,Agent属于一种应用、软件,其核心的设计方式就决定了它在使用过程中的表现。
我们在本文只介绍ReAct模式,实际上还有很多比如Planning、Multi-Agent等模式,也许以后会介绍。。。
ReAct 模式,名字来自 Re asoning + Act ing,是目前最主流的Agent推理框架。核心思想是让大模型在每一步都经历:
Step 1. Thought (思考):分析当前情况,决定下一步
Step 2. Action (行动):调用工具或输出答案
Step 3. Observation (观察):看工具返回了什么
Step 4. 回到Step 1,直到得出最终答案
在这里塞一坨伪代码吧,来自于知乎的一篇文章:D。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 for (let i = 0 ; i < maxLoops; i++) { const { thought, action, args, final } = await llmThink (); if (final) { break ; } const result = await callTool (action, args); context.push (`观察到:${result} ` ); }
这样的话我们大概就知道ReAct的核心要点了,既需要让AI能判断当前问题解决的进度,也需要在AI判断出错时及时终止,这样一来,我们就能让AI真正地帮我们解决一些实际问题而不是只给出建议。
从零实现工具调用 好,前面铺垫够了,现在让我们用Python手搓一个能调用工具的Agent吧 :D。
先调个 API 该系列博客我们使用DeepSeek,理由很简单,我给你最直接、最真相、最不绕弯、最扎心、最硬核、最干脆、最不墨迹、最戳痛点、最不留情面、最一针见血、最开门见山、最单刀直入、最不铺垫、最不客套、最不煽情、最不废话、最不拐弯、最不磨叽、最不装、最不端着、最不啰嗦、最不拖沓、最不委婉、最不掩饰、最不藏着掖着、最直白、最露骨、最实在的结论:便宜,而且 API 和OpenAI完全兼容。
同时,我们使用Python来作为示例代码,理由也很简单,好写。
首先装库:
不要告诉我你还在用 venv/conda 管理环境,快来试试 uv 吧!基于 Rust、快速、轻量
然后写个最简单的对话:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 from openai import OpenAI client = OpenAI( api_key="你的 DeepSeek API Key" , base_url="https://api.deepseek.com" , ) response = client.chat.completions.create( model="deepseek-v4-pro" , messages=[ {"role" : "user" , "content" : "你好,给我讲个冷笑话" } ] )print (response.choices[0 ].message.content)
如果你的openai安装没问题、APIKEY 也没问题、账号确实有余额的话,上面的代码大概率是能输出一个回答的,当然如果考虑要用代码仓库的话,建议像下面把 APIKEY 放到环境变量里。
1 2 3 4 5 6 7 import osfrom openai import OpenAI client = OpenAI( api_key=os.environ.get("DEEPSEEK_API_KEY" ), base_url="https://api.deepseek.com" , )
提示词设计 要让大模型调用工具,第一步是告诉它有哪些工具可以用,我们可以通过构建一个提示词模板来传递。
假设我们给Agent两个工具:查天气和查汇率,让我们先把工具描述清楚:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 TOOLS_DESCRIPTION = """ 你有以下工具可以使用: 1. get_weather(city: str) -> dict 查询指定城市的天气。 参数:city - 城市名称(中文) 返回:{"temp": 温度, "condition": 天气状况} 2. get_exchange_rate(from_currency: str, to_currency: str) -> float 查询汇率。 参数:from_currency - 原货币代码(如 "USD") to_currency - 目标货币代码(如 "CNY") 返回:汇率数值 """
或者我们也可以使用更分层解耦、更高可读性、更高可维护性、更高可拓展性、更工业级、更企业级的写法,对于每一个工具我们都单独地声明它的描述,再统一封装成一个对外的接口,这样我们每一次更新工具描述时,都不用在这个全局变量里改了:D。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 class Tool : name: str = "" description: str = "" parameters: dict = {} async def run (self, params: dict ) -> str : """Execute the tool; return output as a string. Args: params: Arguments supplied by the LLM (conform to `parameters`). """ raise NotImplementedError _DICE_RE = re.compile ( r"(\d+)[dD](\d+)" r"(?:([+-])(\d+))?" , re.IGNORECASE, )class DiceTool (Tool ): name = "dice" description = "掷骰子(例如2d6, 1d20+3)。当用户提到roll、骰子、骰娘、投骰、掷色子等词时调用。" parameters = { "type" : "object" , "properties" : { "notation" : { "type" : "string" , "description" : "骰子表达式,例如'2d6', '1d20+3'。若无明确表达式则传'1d6'。" , } }, "required" : ["notation" ], "return" : { "type" : "string" , "description" : "掷骰结果的文本描述,例如'🎲 2d6 = [3+5] = **8**'" , } } async def run (self, params: dict ) -> str : pass ALL_TOOLS: list [Tool] = [ DiceTool(), ]def render_tool_specs () -> str : """ Return a formatted tool catalog for the LLM system prompt. Format: ## 你可以使用以下工具: 1. tool_name 描述: ... 参数: <JSON schema> """ lines = ["## 你可以使用以下工具:" ] for i, tool in enumerate (ALL_TOOLS, start=1 ): lines.append(f"{i} . {tool.name} " ) lines.append(f" 描述: {tool.description} " ) lines.append(f" 参数: {json.dumps(tool.parameters, ensure_ascii=False )} " ) return "\n" .join(lines)
然后设计系统提示词,告诉大模型它的工作模式:
1 2 3 4 5 6 7 8 9 10 11 SYSTEM_PROMPT = f"""你是一个助手,可以使用工具来回答用户的问题。 {TOOLS_DESCRIPTION} 当你需要使用工具时,用以下格式输出(不要有其他内容): TOOL_CALL: 工具名称 ARGS: {{"参数名": "参数值"}} 当你得到工具返回的结果后,再用自然语言回答用户。 如果不需要工具,直接回答即可。"""
结构化输出的意义在于:我们可以从大模型的回复里可靠地解析出它想调用什么。如果格式不定义清楚,大模型会以各种奇形怪状的方式表达同一个意思,我们就没法很好地解析出来了:D。
用正则表达式解析工具调用 假设大模型已经按我们定义的格式输出了工具调用请求,现在要我们把它解析出来:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import reimport jsondef parse_tool_call (text: str ): """ 从大模型的输出中解析工具调用。 返回 (tool_name, args) 或 None(如果没有工具调用) """ pattern = r"TOOL_CALL:\s*(\w+)\s*\nARGS:\s*(\{.*?\})" match = re.search(pattern, text, re.DOTALL) if not match : return None tool_name = match .group(1 ).strip() args_str = match .group(2 ).strip() try : args = json.loads(args_str) except json.JSONDecodeError: print (f"解析参数失败:{args_str} " ) return None return tool_name, args
然后我们再写几个简单的工具实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 def get_weather (city: str ) -> dict : mock_data = { "上海" : {"temp" : 28 , "condition" : "多云" }, "北京" : {"temp" : 22 , "condition" : "晴" }, "广州" : {"temp" : 33 , "condition" : "阵雨" }, "杭州" : {"temp" : 20 , "condition" : "小雨" }, } return mock_data.get(city, {"temp" : 20 , "condition" : "未知" })def get_exchange_rate (from_currency: str , to_currency: str ) -> float : rates = { ("USD" , "CNY" ): 7.24 , ("EUR" , "CNY" ): 7.85 , ("JPY" , "CNY" ): 0.048 , } return rates.get((from_currency, to_currency), 1.0 ) TOOLS = { "get_weather" : get_weather, "get_exchange_rate" : get_exchange_rate, }def execute_tool (tool_name: str , args: dict ) -> str : if tool_name not in TOOLS: return f"错误:工具 {tool_name} 不存在" try : result = TOOLS[tool_name](**args) return str (result) except Exception as e: return f"执行出错:{e} "
如果使用的是 render_tool_specs 的写法,这里的工具注册和动态调用也需要改一下写法,具体实现就不放出来了,读者可以自己写一下试试:D。
ReAct
ReAct,而不是 React。
好了,现在我们要把所有东西组装成一个完整的 Agent 循环。
ReAct 模式上文已经介绍过了,现在我们实现一下那个伪代码。
用代码实现这个循环:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 def run_agent (user_input: str , max_steps: int = 5 ): """ 运行一个 ReAct Agent。 max_steps 防止死循环。 """ messages = [ {"role" : "system" , "content" : SYSTEM_PROMPT}, {"role" : "user" , "content" : user_input}, ] for step in range (max_steps): print (f"\n--- Step {step + 1 } ---" ) response = client.chat.completions.create( model="deepseek-v4-pro" , messages=messages, ) assistant_message = response.choices[0 ].message.content print (f"模型输出:\n{assistant_message} " ) messages.append({"role" : "assistant" , "content" : assistant_message}) tool_call = parse_tool_call(assistant_message) if tool_call is None : print ("\n✅ 最终答案:" ) print (assistant_message) return assistant_message tool_name, args = tool_call print (f"\n🔧 调用工具:{tool_name} ,参数:{args} " ) result = execute_tool(tool_name, args) print (f"工具返回:{result} " ) messages.append({ "role" : "user" , "content" : f"工具 {tool_name} 的返回结果:{result} " }) print ("⚠️ 达到最大步骤数,强制终止" ) return None
跑起来大概是这样的输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 --- Step 1 --- 模型输出: TOOL_CALL: get_weather ARGS: {"city": "上海"} 🔧 调用工具:get_weather,参数:{'city': '上海'} 工具返回:{'temp': 28, 'condition': '多云'} --- Step 2 --- 模型输出: 上海今天气温28度,多云,非常适合出门!建议穿薄短袖,带一把伞以防傍晚有云转雨。 ✅ 最终答案: 上海今天气温28度,多云,非常适合出门!...
这样整个流程就通了!我们成功实现了一个小型的可以查询天气和汇率且使用了ReAct设计模式的Agent!让我们把上面所有代码整合成完整版:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 import osimport reimport jsonfrom openai import OpenAI client = OpenAI( api_key=os.environ.get("DEEPSEEK_API_KEY" ), base_url="https://api.deepseek.com" , )def get_weather (city: str ) -> dict : mock_data = { "上海" : {"temp" : 28 , "condition" : "多云" }, "北京" : {"temp" : 22 , "condition" : "晴" }, "广州" : {"temp" : 33 , "condition" : "阵雨" }, "杭州" : {"temp" : 20 , "condition" : "小雨" }, } return mock_data.get(city, {"temp" : 20 , "condition" : "未知" })def get_exchange_rate (from_currency: str , to_currency: str ) -> float : rates = { ("USD" , "CNY" ): 7.24 , ("EUR" , "CNY" ): 7.85 , ("JPY" , "CNY" ): 0.048 , } return rates.get((from_currency, to_currency), 1.0 ) TOOLS = { "get_weather" : get_weather, "get_exchange_rate" : get_exchange_rate, } SYSTEM_PROMPT = """你是一个助手,可以使用工具来回答用户的问题。 你有以下工具可以使用: 1. get_weather(city: str) -> dict 查询指定城市的天气。参数:city - 城市名称(中文) 2. get_exchange_rate(from_currency: str, to_currency: str) -> float 查询汇率。参数:from_currency、to_currency - 货币代码(如 "USD"、"CNY") 当你需要使用工具时,用以下格式输出(不要有其他内容): TOOL_CALL: 工具名称 ARGS: {"参数名": "参数值"} 得到工具结果后,再用自然语言回答用户。不需要工具时直接回答。""" def parse_tool_call (text: str ): """ 从大模型的输出中解析工具调用。 返回 (tool_name, args) 或 None(如果没有工具调用) """ pattern = r"TOOL_CALL:\s*(\w+)\s*\nARGS:\s*(\{.*?\})" match = re.search(pattern, text, re.DOTALL) if not match : return None tool_name = match .group(1 ).strip() args_str = match .group(2 ).strip() try : args = json.loads(args_str) except json.JSONDecodeError: print (f"解析参数失败:{args_str} " ) return None return tool_name, argsdef execute_tool (tool_name: str , args: dict ) -> str : if tool_name not in TOOLS: return f"错误:工具 {tool_name} 不存在" try : result = TOOLS[tool_name](**args) return str (result) except Exception as e: return f"执行出错:{e} " def run_agent (user_input: str , max_steps: int = 5 ): """ 运行一个 ReAct Agent。 max_steps 防止死循环,别让它一直转。 """ messages = [ {"role" : "system" , "content" : SYSTEM_PROMPT}, {"role" : "user" , "content" : user_input}, ] for step in range (max_steps): print (f"\n--- Step {step + 1 } ---" ) response = client.chat.completions.create( model="deepseek-v4-pro" , messages=messages, ) assistant_message = response.choices[0 ].message.content print (f"模型输出:\n{assistant_message} " ) messages.append({"role" : "assistant" , "content" : assistant_message}) tool_call = parse_tool_call(assistant_message) if tool_call is None : print ("\n✅ 最终答案:" ) print (assistant_message) return assistant_message tool_name, args = tool_call print (f"\n🔧 调用工具:{tool_name} ,参数:{args} " ) result = execute_tool(tool_name, args) print (f"工具返回:{result} " ) messages.append({ "role" : "user" , "content" : f"工具 {tool_name} 的返回结果:{result} " }) print ("⚠️ 达到最大步骤数,强制终止" ) return None if __name__ == "__main__" : run_agent("1美元等于多少人民币?" ) run_agent("北京今天适合骑车上班吗?" )
最后我们运行一下,一切顺利的话我们就可以得到类似如下的输出:
总结 相当顺利的一篇博客 啊 过程中使用了大量的 AI 辅助创作 致歉🙏
在写这篇博客的时候,A\正好发布了新的模型,效果超强啊,其实 Claude Code 写代码效果好的原因不只是因为 A\ 的模型好,它内部的 Agent 的设计模式是绝对优秀且模范的,正好它的源码泄露过,有机会我们就写一下它的源码解析?(不清楚犯不犯法
然后是Cesium教程估计会长期停更,毕竟我已经跳槽到其他行业了:D。在名GOD 的提醒下想起来了被我鸽掉的GeoHash,等恢复Cesium更新的时候放在一起讨论罢。不得不说两个月时间换了三份工作,从前端到小程序全栈再到Java,这份经历也是挺惊奇的,有时间再写一篇跳槽心得?
然后是 Agent 系列会长期更新,毕竟是我真实感兴趣的,第二集已经决定了写 RAG,不过下一篇也许会写一个关于算法竞赛的回忆录,毕竟算是退役了嘛,以后也许还是会打一打算法竞赛,也许会直接在博客里分享一些有意思的算法、题解之类的。
最后是本文借鉴参考的一些文章:
AI Agent 设计模式 - ReAct 模式