ARTICLE DETAIL

资讯详情

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

DeepSeek-Agent-Harness-2026终极指南-第8章第35节-AgentLoop从零实现-最小可用Agent循环:while+tool_calls的50行魔法

DeepSeek-Agent-Harness-2026终极指南-第8章第35节-AgentLoop从零实现-最小可用Agent循环:while+tool_calls的50行魔法 DeepSeek Agent Harness 2026终极指南 - 第8章第35节 最小可用 Agent 循环while tool_calls 的 50 行魔法日志留痕体系完工了理论区讲了四节 Agent Loop 原理——现在是动手的时候。这节用 50 行代码实现 DeepPilot 的第一个完整 Agent Loop组装消息 → 调模型 → 解析 tool_calls → 执行工具 → 回填结果 → 再调模型循环直到模型说行了完工。你会亲眼看到 Agent 的第一次呼吸——它先告诉你我得查天气查完回来告诉你结果。不再是调一次 API 等一个回答而是真正的推理-行动-观察循环。本文导航Agent Loop 的心跳while tool_callsget_weather一个愿意被调用的工具47 行核心agent_loop.py 完整实现逐行拆解每一行在干什么完整实录亲眼看到 ReAct 循环小结Agent Loop 的心跳while tool_calls回顾第 16 节的 ReAct 循环原理1. 模型收到用户问题 2. 模型思考Thought我需要查天气 3. 模型行动Action调 get_weather(北京) 4. 系统观察Observation晴天28°C 5. 把观察结果回填给模型 6. 模型再次思考有数据了可以回答 7. 模型输出最终答案第 2-6 步是一个循环。现实中它可能跑 1 轮、3 轮、甚至 10 轮取决于用户问了几个需要工具的问题。把它翻译成代码骨架messages[{role:user,content:北京天气怎么样}]whileTrue:# 第 2-3 步调模型responseclient.chat(messages,toolstools)# 第 7 步没有 tool_calls → 模型回答完了退出ifnotresponse.tool_calls:print(response.content)break# 第 4 步解析 tool_calls执行工具fortcinresponse.tool_calls:tool_nametc[function][name]argsjson.loads(tc[function][arguments])resultexecute_tool(tool_name,args)# 真实执行# 第 5 步回填messages.append({role:tool,tool_call_id:tc[id],content:result})# 第 6 步把 assistant 消息含 tool_calls也追加进去messages.append(assistant_msg)# 回到第 2 步去掉注释和空行纯逻辑不到 15 行。再加上工具定义、消息组装、输出美化——总共 47 行就是完整可运行的 Agent Loop。get_weather一个愿意被调用的工具在进入完整 Loop 之前先把工具准备好。这里不搞复杂的工具注册中心那是第 36 节的事就一个简单到不能再简单的函数# deep_pilot/tools.py —— v0.3 最小工具集defget_weather(city:str)-str:获取指定城市的天气模拟数据weather_db{北京:晴28°C湿度 45%北风 3 级,上海:多云32°C湿度 70%东南风 2 级,深圳:雷阵雨26°C湿度 85%西南风 4 级,东京:阴22°C湿度 60%东北风 3 级,纽约:晴18°C湿度 40%西风 5 级,}returnweather_db.get(city,f未找到{city}的天气数据请确认城市名。)# 工具注册表——名字到函数的映射TOOLS_MAP{get_weather:get_weather,}# Function Calling 需要的 tools 参数——描述给模型看的说明书TOOLS_SCHEMA[{type:function,function:{name:get_weather,description:获取指定城市今天的天气信息。输入城市中文名返回天气描述。,parameters:{type:object,properties:{city:{type:string,description:城市中文名例如北京、上海、深圳,}},required:[city],},},}]三件套get_weather()是工具的实现体。这里是 mock 数据生产环境换成真实 API。TOOLS_MAP是工具注册表——{name: function}。Loop 拿到tool_calls后根据 name 找到对应的函数。TOOLS_SCHEMA是 JSON Schema 格式的工具描述。每次调模型时作为tools参数传进去模型会根据描述决定该不该用这个工具和参数怎么填。47 行核心agent_loop.py 完整实现# deep_pilot/agent_loop.py —— v0.3 最小可用 Agent Loopfrom__future__importannotationsimportjsonfromtypingimportAnyfromdeep_pilot.clientimportclientfromdeep_pilot.loggerimportget_loggerfromdeep_pilot.toolsimportTOOLS_MAP,TOOLS_SCHEMA loggerget_logger(__name__)MAX_ITERS10# 防止死循环——第 18 节讲的强制终止defrun(user_query:str,tools:list[dict]|NoneNone)-str: 核心 Agent Loop消息 → 模型 → tool_calls → 执行 → 回填 → 循环。 返回模型的最终文本回复。 toolstoolsorTOOLS_SCHEMA messages:list[dict[str,Any]][{role:system,content:你是一个有用的助手。当需要查询实时数据时使用提供的工具。},{role:user,content:user_query},]foriterationinrange(1,MAX_ITERS1):logger.info(fLoop 第{iteration}轮 ↻)# 第 2 步调模型respclient.chat(messages,toolstools)# 第 7 步没有 tool_calls → 模型给出最终答案退出ifnotresp.tool_calls:returnresp.contentor# 第 4-6 步处理 tool_calls# 先把 assistant 消息含 tool_calls追加到历史messages.append({role:assistant,content:resp.content,tool_calls:resp.tool_calls,})fortcinresp.tool_calls:func_nametc[function][name]func_argsjson.loads(tc[function][arguments])logger.info(f → 调用工具:{func_name}({json.dumps(func_args,ensure_asciiFalse)}))tool_funcTOOLS_MAP.get(func_name)iftool_funcisNone:resultf错误未知工具 {func_name}else:try:resulttool_func(**func_args)ifnotisinstance(result,str):resultjson.dumps(result,ensure_asciiFalse)exceptExceptionase:resultf工具执行失败:{e}logger.info(f ← 工具结果:{result[:80]}{...iflen(result)80else})# 回填 tool 角色消息messages.append({role:tool,tool_call_id:tc[id],content:result,})# 循环回到第 2 步——messages 里已经有了 assistant(tool_calls) tool 回填# 超过 MAX_ITERS 仍未退出——强制终止logger.warning(fAgent Loop 达到最大迭代次数{MAX_ITERS}强制终止)return[Agent] 抱歉处理超时。请简化问题后重试。逐行拆解每一行在干什么第 14-16 行组装初始消息messages[{role:system,content:你是一个有用的助手...},{role:user,content:user_query},]初始 messages 只有 system user 两条。system 提示词里强调当需要查询实时数据时使用工具——这很重要否则模型不知道应该调工具可能直接瞎编一个天气。第 21 行调模型respclient.chat(messages,toolstools)用的就是第 28 节封装的DeepSeekClient.chat()。tools参数是TOOLS_SCHEMA模型看到这个 schema 就知道有一个叫get_weather的函数可用。第 24-25 行判断终止ifnotresp.tool_calls:returnresp.contentor模型返回finish_reasonstop且没有tool_calls→ 说明它已经回答了问题、不需要更多工具。直接返回内容结束循环。第 28-32 行追加 assistant 消息messages.append({role:assistant,content:resp.content,tool_calls:resp.tool_calls,})这里有一个很多人踩的坑——assistant 消息必须原样保留 tool_calls 字段。如果你只存 content 不存 tool_calls下一轮调模型时会报错因为模型期望看到完整的对话历史。这个坑我在第 17 节讲过现在亲身踩到了。第 34-48 行执行工具 回填fortcinresp.tool_calls:func_nametc[function][name]func_argsjson.loads(tc[function][arguments])tool_funcTOOLS_MAP.get(func_name)resulttool_func(**func_args)messages.append({role:tool,tool_call_id:tc[id],content:result,})注意三点json.loads(tc[function][arguments])——OpenAI 协议里arguments是 JSON字符串不是 dict必须反序列化。tool_func(**func_args)——用**展开参数字典传给函数。get_weather(**{city: 北京})等同于get_weather(city北京)。tool_call_id: tc[id]——回填消息必须带tool_call_id对账。模型在 assistant 消息里声明了一个 idcall_xxxtool 回填时要把这个 id 带回来模型才知道这个工具结果对应我之前的哪个调用请求。第 52-54 行强制终止logger.warning(fAgent Loop 达到最大迭代次数{MAX_ITERS})return[Agent] 抱歉处理超时。如果走了 10 轮还是没停比如模型一直要求调工具但工具一直返回空强制终止。这是第 18 节讲的停机三闸之一。第 20 节的三道熔断会在后续版本加入。完整实录亲眼看到 ReAct 循环cddeep-pilot# 写一个入口脚本uv run python-c from deep_pilot.agent_loop import run # 简单问题——不需要工具 print( 简单问题无需工具) answer run(你好11等于几) print(f最终答案: {answer}) print() # 需要查天气——触发工具调用 print( 需要查天气触发工具调用) answer run(北京今天天气怎么样) print(f最终答案: {answer}) print() # 查多个城市——多轮工具调用 print( 查多个城市多轮工具调用) answer run(北京和上海的天气对比一下) print(f最终答案: {answer}) 控制台输出精简了日志前缀2026-09-12 16:00:01 | INFO | agent_loop | Loop 第 1 轮 ↻ 2026-09-12 16:00:01 | INFO | client | 调用留痕 | call_id111aaa... | ... 最终答案: 11等于2。 2026-09-12 16:00:02 | INFO | agent_loop | Loop 第 1 轮 ↻ 2026-09-12 16:00:02 | INFO | client | 调用留痕 | call_id222bbb... | ... 2026-09-12 16:00:02 | INFO | agent_loop | → 调用工具: get_weather({city: 北京}) 2026-09-12 16:00:02 | INFO | agent_loop | ← 工具结果: 晴28°C湿度 45%北风 3 级 2026-09-12 16:00:02 | INFO | agent_loop | Loop 第 2 轮 ↻ 2026-09-12 16:00:02 | INFO | client | 调用留痕 | call_id333ccc... | ... 最终答案: 北京今天天气晴28°C湿度 45%北风 3 级。适合出行 2026-09-12 16:00:04 | INFO | agent_loop | Loop 第 1 轮 ↻ 2026-09-12 16:00:04 | INFO | client | 调用留痕 | call_id444ddd... | ... 2026-09-12 16:00:04 | INFO | agent_loop | → 调用工具: get_weather({city: 北京}) 2026-09-12 16:00:04 | INFO | agent_loop | ← 工具结果: 晴28°C 2026-09-12 16:00:04 | INFO | agent_loop | Loop 第 2 轮 ↻ 2026-09-12 16:00:04 | INFO | client | 调用留痕 | call_id555eee... | ... 2026-09-12 16:00:04 | INFO | agent_loop | → 调用工具: get_weather({city: 上海}) 2026-09-12 16:00:04 | INFO | agent_loop | ← 工具结果: 多云32°C 2026-09-12 16:00:04 | INFO | agent_loop | Loop 第 3 轮 ↻ 2026-09-12 16:00:04 | INFO | client | 调用留痕 | call_id666fff... | ... 最终答案: 北京今天晴28°C适合出行。上海多云32°C比较闷热。建议今天去北京的户外活动。 DeepPilot 本次运行调用总结 调用次数: 6 指标 最小 最大 平均 合计 -------------------- ---------- ---------- ---------- ---------- 输入 token 8 42 21.3 128 输出 token 6 185 73.8 443 总 token 14 221 94.7 568 耗时(秒) 0.89 3.45 1.89 终止原因分布: {stop: 6} 模型使用分布: {deepseek-flash: 6} 看整个过程简单问题 111 轮 Loop → 模型直接回答没调工具查北京天气2 轮 Loop → 第 1 轮模型说我要查 weather→ 工具执行 → 第 2 轮模型综合结果给出回答对比北京上海3 轮 Loop → 第 1 轮查北京 → 第 2 轮查上海 → 第 3 轮综合两个结果做对比分析这就是 ReAct 循环的具体表现。47 行代码Agent 已经不再是调一次 API 等一个回答而是有思考-行动-观察的完整推理链。小结Agent Loop while tool_calls。核心逻辑 15 行加上工具定义和输出美化一共 47 行。循环四步调模型 → 判终止无 tool_calls→ 执行工具 → 回填结果。assistant 消息必须原样保留 tool_calls否则下一轮模型不认。tool 回填必须带 tool_call_id用于对账。arguments是 JSON 字符串必须json.loads。MAX_ITERS 强制终止防止死循环第 18 节停机三闸之一。多轮工具调用时 messages 自动膨胀每轮追加 assistant tool 两条模型能记住之前调用过什么工具、拿到了什么结果。DeepPilot v0.3 Agent Loop 核心引擎就位——50 行代码Agent 有了第一次呼吸。下节预告目前的工具体系还很粗糙——TOOLS_MAP手动维护TOOLS_SCHEMA 手写 JSON。下一节做真正的工具注册中心用tool装饰器自动注册工具函数用 pydantic 反射自动生成 JSON Schemamodel_json_schema()三行代码即可注册一个新工具。从此加工具不再手写 JSON装饰器一加就能用。如果觉得本文对你有帮助欢迎点赞、收藏、关注三连本系列持续更新中关注不迷路~
返回列表