news 2026/10/21 11:57:23

从零手写最小AI Agent:核心循环与工具调用全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零手写最小AI Agent:核心循环与工具调用全解析

AI Agent学习日记写到Day4,状态比前两天踏实了不少。前三天我基本在赶进度:第一天搞懂了大模型对话API的基本格式、token和Prompt工程;第二天啃完了Agent的基础概念,理解了“工具调用”“记忆”“规划”这些词的大致含义;第三天跟着教程跑通了一个现成Agent框架的Demo,还给Agent配了两个工具,让它帮我查天气、算数据。问题也出在第三天——框架确实能跑,但日志刷出来的时候我心里全是疑问:到底是什么在决定“该查天气了”?参数是怎么从一个函数传到另一个函数的?为什么Agent算完数据之后,还记得我最开始问的是什么?所以Day4我做了一个决定:把框架暂时放一边,只用几十行Python,从零手写一个最小可用的Agent。这篇文章就是Day4的记录,目标是彻底讲透Agent最核心的“思考→调用工具→观察结果”循环,代码会完整给出,不藏私。

1. 为什么第4天我宁可先放下框架

1.1 前三天到底学了什么

第一天是最基础的:搞清楚什么是token,什么是completion,怎么样在本地用API把一个Prompt发给大模型,再把返回的文本打印出来。那天我最大的收获是知道大模型本质上是一个“概率文本生成器”,你给它一段文字,它基于训练学到的规律,继续生成后面的文字。

第二天进入Agent的正题。我把“Agent”的定义捋了一遍:与普通ChatBot的区别在于,它能自己做决策、调用外部工具、感知结果、调整下一步动作。当时我在笔记本上写了一个自认为很精准的总结——“Agent = LLM(大脑)+ 工具(手脚)+ 循环(行动框架)”,但现在回头看,这句话只是记住了,并没有真正理解。

第三天用的是某个现成的Agent框架。我按照教程配置了一个Agent,挂上两个工具,跑起来确实很快。但越是“开箱即用”,我越觉得心虚。我看不到“谁调了谁”的细节,遇到问题唯一能做的就是改Prompt重试。那天结束后我列了一个问题清单:工具Schema是怎么被模型看到的?模型返回的tool_call谁去执行?执行完结果通过什么方式回到模型?于是Day4干脆自己造一个简单的轮子。

1.2 框架带来的“认知黑洞”

用框架最大的问题不是它不好,而是它把关键路径封装得太干净。你在框架里注册一个函数,框架自动帮你生成Schema,自动处理一轮又一轮的对话历史,自动把工具结果塞回上下文。你只看到最后输出了一个答案,但中间每一步是怎么衔接的,完全是个暗箱。

我可以类比一下:框架像是给你一辆能自动驾驶的车。你上车、输入目的地、车自己开到了。你坐得很舒服,但让你自己把车拆了再装回去,你根本不知道发动机怎么点火,变速箱怎么换挡。如果你想做Agent方向的开发,这种状态是危险的。因为真实项目里你会遇到各种边缘情况:模型返回了不规范的参数、工具执行报错、上下文撑爆、模型在一个死循环里反复调同一个工具。这些问题,框架帮不了你,你必须懂原理自己去处理。

所以Day4我给自己定的规则很死:不装任何Agent框架,不用任何封装好的Agent类,只用基础的HTTP调用和Python函数,手写一个能让模型调用工具并继续思考的最小循环。代码量预计100行以内。

1.3 手搓Agent到底是在搓什么

先说结论:手搓一个最小Agent,本质上是写一个短小的while循环。这个循环大致长这样——

  1. 把用户的原始问题拼上系统提示词,发给大模型;
  2. 模型返回两种情况:要么直接给出最终回答,要么要求调用某个工具;
  3. 如果是要调工具,我们在本地代码里真正执行这个函数,把返回结果转成文本,塞回消息列表;
  4. 带着新的工具结果,再次调用模型;
  5. 重复上面步骤,直到模型认为信息足够、直接给出最终回答,或者达到我们设置的最大步数。

这就是Agent最原始的内核。所有花里胡哨的框架,围绕的其实就是这个循环,只是在记忆管理、工具编排、规划策略、多轮对话维护上做了更多增强。所以想理解Agent,别从增强开始,先把这个最原始的内核剪出来,看清楚它怎么运转。

2. 核心循环拆解:想清楚“思考、做事、看结果”这三步

2.1 大模型只负责“想”,不负责“做”

这是我在Day4最开始给自己重复了十遍的一句话。很多人误以为Agent是大模型自己动手操作了什么,其实不是。

大模型的能力边界非常清楚:它只负责“生成文本”。你给它一段Messages,它返回一段内容。即便它“调用工具”,本质也只是在返回的文本里夹带了一个结构化的请求,比如“我想调用calculator这个工具,参数是expression: 14 * 2”。这个请求并不会自己执行。真正执行计算、去查天气、把结果拿回来的,是模型之外的一段普通代码。

我把这种配合叫“决策和执行分离”。模型的角色像一个团队里的负责人,它根据掌握的信息决定“下一步应该做什么”;我们的代码像执行层,负责真正把事办成,并把结果汇报给负责人。负责人看完汇报,再继续决策,直到它觉得任务完成。

明白这一点之后,看框架的日志就不再玄学了。日志里每一行“tool call”其实都包含两个部分:模型发了个调用申请,你的程序执行了这个申请并把结果写回上下文。仅此而已。

2.2 用一次查天气的过程理解循环

举个例子,假设用户问:“北京今天适合穿羽绒服吗?”Agent内部可能发生这样几轮对话。

第一轮,模型看到这个问题,判断“我不知道实时天气”,于是输出一个工具调用请求:查询某城市天气。我们本地代码收到请求,真正去天气接口拉数据,把结果包装成一条消息回给模型。这条消息里写:北京今天晴,最高温零下2度,西北风4级。

第二轮,模型看到天气数据,结合常识判断:零下2度且风大,适合穿羽绒服。于是它不再调用工具,直接生成最终回答:建议穿厚款羽绒服,最好再戴个帽子和围巾。

整个过程,模型没有主动访问网络,真正查天气的是我们代码里那个函数,但“根据天气回答是否穿羽绒服”是模型做的推理判断。这就是Agent和普通聊天最本质的区别:它有“感知外部世界→行动→获得反馈→继续推理”的能力,而聊天机器人只有嘴,没有手。

2.3 终止条件与死循环防护

写循环最怕什么?怕它停不下来。模型如果一直在“调用工具→看到结果→继续调用工具”里绕圈,你的账单和Latency都会很难看。所以Agent循环一定需要两个终止条件。

第一个终止条件是自然的:模型返回的内容里不再包含工具调用请求,只有最终回答文本,说明它认为任务已经完成,此时循环结束。第二个终止条件是人设的保险丝:max_steps。我在代码里设成6,意思是最多循环6轮,超过就直接停止并报错。这能防止模型因为某种原因陷入死循环。

顺便提醒一句:max_steps设多大要看任务复杂度。只查一次天气,2到3步就够;需要多工具配合的任务,可以放大到8到10。但不要无脑设成100,工具结果越多,上下文越长,后面的每一次调用都在燃烧tokens,成本也会失控。

2.4 工具注册:模型看到的和真实运行的可能是两套东西

关于工具调用,我第三天的疑惑点之一是“模型怎么知道有哪些工具可以用”。Day4我翻开了底层协议,才彻底明白:你在框架里注册函数时,框架会把这个函数翻译成一份JSON Schema,然后跟着请求一起发给模型。

这份Schema描述的是“工具长什么样”:函数名、功能说明、参数名、参数类型、哪些参数必填、分别是什么意思。模型不看函数源码,只看这份说明书。所以工具描述写得好不好,直接决定模型会不会在正确时机调用它。

但这又带来一个容易混淆的点:模型看到的“calculator”是一个符号化的名字和描述,而真实执行时,我们的代码拿这个名字去一个本地字典里找到真正的Python函数,然后调用。也就是说,Schema是模型侧的说明书,真实函数是执行侧的实体,中间靠名字关联起来。这个映射关系,Agent框架帮你藏了起来,手写的时候必须自己维护。

3. 手写一个最小Agent:代码全放送

3.1 环境准备与LLM接入

我的运行环境是Python 3.10,额外用到了requests库发送HTTP请求。为了可复现,我直接把LLM接入封装成一个函数,通过环境变量指定接口地址和模型名。

很多推理服务都提供通用的Chat Completions协议,所以下面这层封装不绑定特定的服务商,你在本地起一个兼容该协议的推理服务,或者用任何支持该协议的云服务,都能跑通。

import json import os from datetime import datetime import requests LLM_BASE = os.environ.get("LLM_BASE", "http://127.0.0.1:8000/v1/chat/completions") MODEL_NAME = os.environ.get("MODEL_NAME", "local-demo-model") def call_llm(messages, tools=None): payload = { "model": MODEL_NAME, "messages": messages, "temperature": 0.2, } if tools: payload["tools"] = tools payload["tool_choice"] = "auto" resp = requests.post(LLM_BASE, json=payload, timeout=120) resp.raise_for_status() return resp.json()["choices"][0]["message"]

两点说明:temperature我刻意调成了0.2,目的是让模型尽量稳定地按协议输出,少一点随机发挥;timeout设成120秒是因为一些工具任务可能需要多次内部推理,短了容易误报超时。后面踩坑部分我会详细说temperature过高会发生什么。

3.2 两个练手工具:取本地时间和计算器

我给Agent准备了两个不需要额外依赖的工具。第一个是获取当前本地时间,纯本地函数,任何人复现都能运行;第二个是一个简单计算器,能处理加减乘除和括号。这两个工具足够演示“多工具协作”的场景:比如我问“现在几点了?把小时数乘以2得到一个新数字”,Agent需要先调时间工具,再调计算器,中间还有一次结果回传。

def get_current_time(): """获取当前本地时间""" return {"now": datetime.now().strftime("%Y-%m-%d %H:%M:%S")} def calculator(expression): """计算数学表达式的结果,支持 + - * / 和括号""" # 只允许数字、四则运算符、括号和空白 if not re.fullmatch(r"[\d\s+\-*/().]+", expression): return {"error": "表达式包含不支持的字符"} try: result = eval(expression, {"__builtins__": {}}, {}) except Exception as exc: return {"error": str(exc)} return {"result": result}

注意:这里的eval仅用于本地演示。生产环境绝对不能直接eval用户输入,会带来非常严重的注入风险。如果要做一个可靠的计算器,我建议用AST解析或者专门的计算库,把允许的节点类型白名单化,再执行。我在演示代码里已经用正则限制了字符集,同时清空了内置函数,但这两层只能挡住最简单的攻击,不要把它当成安全方案。

3.3 工具JSON Schema:给模型看的“使用说明书”

要让模型知道可以调用什么工具,得按协议把工具描述成JSON Schema。这一步也是“框架在背后偷偷帮你做的事”。我手动写了一遍之后才明白,为什么工具描述那么重要:模型是根据这段文字去决定调用时机的。

TOOLS = [ { "type": "function", "function": { "name": "get_current_time", "description": "获取当前本地日期和时间,不需要任何参数", "parameters": {"type": "object", "properties": {}}, }, }, { "type": "function", "function": { "name": "calculator", "description": "计算数学表达式的结果,支持四则运算和括号。输入必须是合法的表达式,例如 '13 * 2'", "parameters": { "type": "object", "properties": { "expression": {"type": "string", "description": "待计算的数学表达式"} }, "required": ["expression"], }, }, }, ]

写这份Schema我踩的第一个感知上的坑是:模型并不执行函数,它只负责生成“符合这个Schema的参数”。它可能生成 expression = "13 * 2",但不会真的去计算。真正计算发生在我的Python函数里。所以Schema是双份约定:模型用它来生成参数,我的代码用它来校验和映射参数。两边必须对得上。

3.4 核心循环:run_agent完整实现

下面是整个Day4最核心的代码。我把工具名到真实函数的映射放在一个字典里,然后写一个run_agent函数,里面维护一个messages列表,循环发请求、判断是否要调用工具、执行工具、把结果回填到消息列表。

TOOLS_MAP = { "get_current_time": get_current_time, "calculator": calculator, } SYSTEM_PROMPT = ( "你是一个AI助手。如果用户的问题需要实时信息或计算,请先调用工具。" "工具返回后,基于工具结果继续思考和回答。" "不要编造工具结果。如果不需要工具,就直接回答用户。" ) def run_agent(user_question, max_steps=6): messages = [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": user_question}, ] for step in range(max_steps): message = call_llm(messages, tools=TOOLS) messages.append(message) if not message.get("tool_calls"): print("[Agent] 最终回答:", message["content"]) return message["content"] for tool_call in message["tool_calls"]: name = tool_call["function"]["name"] arguments = json.loads(tool_call["function"]["arguments"]) print(f"[Step {step + 1}] 调用工具:{name},参数:{arguments}") result = TOOLS_MAP[name](**arguments) print(f"[工具结果] {result}") messages.append( { "role": "tool", "tool_call_id": tool_call["id"], "name": name, "content": json.dumps(result, ensure_ascii=False), } ) print("[Agent] 达到最大步数,停止。") return None if __name__ == "__main__": run_agent("现在几点了?把这个小时数乘以 2,得到一个新数字。")

这个函数里有几个细节我后来复盘时觉得特别关键:

第一,messages列表是贯穿整个循环的状态容器。用户的原始问题、模型每次的输出、工具返回的结果,全都被追加进去,再作为下一轮的输入发给模型。这保证了模型“记得住”自己刚才调过什么工具、得到过什么结果。

第二,每个工具结果的message必须带tool_call_id。这个ID来自模型刚才发出的tool_call请求,它相当于一个关联凭证,让模型知道这个结果是对应哪一次调用。少了它,绝大多数的接口都会直接报错。

第三,执行工具用的是TOOLS_MAP[name],相当于一个动态分发。模型传过来的工具名是字符串,我们用它去查字典、拿到真实函数、再用**arguments展开参数调用。这就把模型侧的抽象符号和真实执行对齐了。

3.5 跑一次真实任务,盯着中间过程看

我拿“现在几点了?把这个小时数乘以2,得到一个新数字”这个问题去跑。注意这个问题故意设计成需要连续调用两个工具:先取时间,再从时间里拆出小时数,然后计算乘2。运行日志大致是这样的:

[Step 1] 调用工具:get_current_time,参数:{} [工具结果] {'now': '2025-01-15 14:27:03'} [Step 2] 调用工具:calculator,参数:{'expression': '14 * 2'} [工具结果] {'result': 28} [Agent] 最终回答: 现在时间是14点27分,小时数是14,乘以2以后是28。

看这个日志,Agent的循环逻辑就很直观了。第一步,模型发现“现在是几点”无法靠自身知识回答,于是发起get_current_time调用;我们代码执行完,把结果塞回messages。第二步,模型看到当前时间是14点27分,它自己完成了“提取小时数14”这个简单推理,然后认为“14 * 2”需要用计算器,再次发起调用。最终拿到结果28,它觉得足够了,就开始组织自然语言回答。

最让我有感觉的是第二步里“14”这个数字的提取。没有任何代码去解析这个时间字符串,是模型自己从工具结果里读出来的。这说明工具结果只要以文本形式回到messages里,模型就能理解并利用它。这也是为什么工具返回的content建议用JSON转成字符串:模型看到的是文本,让它从结构化的文本里取信息,比直接塞一个Python对象更符合它的输入方式。

3.6 如果模型不支持原生工具调用,用文本协议也能顶住

原生工具调用协议很香,但不是所有模型都支持。Day4我还顺手测了一下手边的某个本地小参数模型,它对tools参数几乎无感,直接忽略。这时候还有第二种方案:让模型在回答里输出结构化的文本块,我们自己解析。

思路是改造System Prompt,要求模型在需要工具时,严格输出如下格式:

{"thought": "我需要先获取当前时间", "action": "get_current_time", "action_input": {}}

然后代码如下:

import re TEXT_PATTERN = re.compile(r"\{.*?\}", re.DOTALL) def parse_action(content): match = TEXT_PATTERN.search(content) if not match: return None return json.loads(match.group(0))

拿到解析结果后,做和原生工具调用一样的事情:执行函数、塞结果回messages、再次调用模型。区别只是少了几行协议层的自动解析,多了一部分自己维护的字符串解析逻辑。这个方案是文本版ReAct的雏形,也是很多早期Agent项目跑通的方式。理解了它,你再看任何框架的“agent internals”日志都会觉得亲切许多。

4. Day4踩坑实录:每一个都值得写进笔记

4.1 把arguments当成JSON对象直接用

我第一个踩的坑就是这个。模型返回的tool_calls里,function下层的arguments是一个字符串,例如'{"expression": "14 * 2"}'。我第一次写代码时觉得“这看起来就是对象嘛”,直接**arguments展开,结果TypeError扑面而来。

原因是协议里为了兼容性,把arguments序列化成了字符串。你必须先json.loads一下,再传给函数。后来我建议你在loads外面套一层try-except。因为模型偶尔会生成不合法的JSON,比如多一个逗号、漏一个引号。如果解析失败,可以把“你的参数不是合法JSON”作为一条错误信息回传给模型,让它重新生成调用请求。这样比直接让程序崩溃聪明得多。

4.2 忘记回传tool_call_id,接口直接报错

第二个坑在第一次手写循环时几乎必踩。我给messages追加工具结果时,只写了role、name、content,忘了写tool_call_id,结果接口返回状态码400,提示工具结果与调用无法匹配。

这个ID的作用是关联:模型在上一轮发起了一个调用请求,每个请求都有自己的ID。工具结果带上这个ID,模型才能知道“这个返回是对应我哪个调用的”。如果你不传,它就不知道哪个工具执行完了。所以后面一旦遇到类似报错,我第一反应就是检查工具结果消息里有没有tool_call_id,字段名拼错没有,ID有没有拿错。

4.3 模型不调工具,或者反复调同一个工具

Day4的另一个头疼点是:在某些测试里,模型明明该调计算器,却直接凭空口算了一个错误答案;另一些测试里,它又像魔怔了一样反复调同一个工具,哪怕结果已经拿到了。

排查方向我是这样整理的:

现象排查方向一句话解法
明明需要工具却不调工具描述不够具体;问题中隐含的“计算/查询”信号弱在description里写清楚“什么时候用”和“输入格式”
反复调同一个工具temperature过高;工具返回信息不足以触发下一步决策把temperature降到0.2左右,查看工具结果是否完整回传
编造工具返回值system prompt没强调禁止编造明确写“不要编造工具结果,只能基于返回内容回答”
传了明显不合理的参数参数描述不清楚;required字段不完整在Schema里把每个参数的语义和格式写清楚

还有一个细节:工具描述里的“什么时候不要用”往往被人忽略。比如计算器只处理数学表达式,你如果不写清“只处理表达式,不处理单位的换算”,模型可能在用户问“2米加3分米等于多少”时傻傻地把2+3传给计算器。描述写清楚边界,工具被误用的概率会低很多。

4.4 工具返回巨大,上下文长度爆掉

这是Agent工程里躲不开的问题。单个工具返回几十行甚至上百行的JSON,几次调用下来,messages里的文本量急剧膨胀。我第三次跑测试时就碰上了,模型连续查了几个工具,每个结果都塞回上下文,第4轮还没跑完,接口就报超出上下文长度限制。

我Day4用的解法比较朴素:限制历史长度。比如维护一个“最近N轮消息”的滑动窗口,只保留最近的几轮工具结果和模型输出,丢弃最早的。另外,对于大型工具返回,可以先在代码层做一个摘要,比如只保留关键字段,而不是把整个JSON原样塞回去。甚至有经验的开发者会把大结果先丢给模型做一个压缩,再把压缩后的文本存进上下文。这些都属于记忆管理的范畴,Day5我会继续深入。但你从Day4起就该知道:无限往messages里追加数据是撑不久的。

4.5 工具执行抛异常时,别让Agent整个崩掉

工具函数是普通代码,当然可能抛异常。我第一次测试时,计算器收到一个格式奇怪的表达式抛了异常,程序直接traceback结束,挺尴尬的。后来我改成在函数内部try-except,把异常信息转换成{"error": "..."}返回。这样模型看到的是“工具执行失败”的文本,它会自己判断是重新尝试还是向用户说明。

这里隐藏着一个Agent设计的重要原则:工具失败不应该让整个对话崩溃。把异常当成一种普通结果回传到模型层,让模型来决策,比鲁莽地中断循环更合理。这也是Agent鲁棒性的来源之一。框架里那些“优雅降级”“自我纠错”的能力,追根溯源就是在这一层做了处理。

5. 收尾:今天这个小循环,明天能长出什么

5.1 最大的认知更新

Day4跑通这个最小Agent之后,我对之前那些概念的理解发生了质变。以前我以为Agent复杂,是因为框架把编排、记忆、规划这些词堆在一起,显得很高深。现在亲手写了一遍循环,我发现最底层的骨架其实极其简单:一个while循环、一个messages列表、一个工具映射字典,没了。

所谓框架的“智能体能力”,本质上是在这个骨架外面做了更多轮的策略控制:它可能帮你裁剪历史、自动生成子任务计划、在多步骤之间插入反思节点、或者同时维护多个对话上下文。但这些都不能改变核心循环的本质——模型决策一次,我们执行一次,结果再喂回模型。有了这个认知,以后看任何框架的源码都有切入点。

5.2 Day5之后想继续扩展什么

今天这个小循环已经能处理“取时间→计算→回答”这种多工具协作的场景。但它仍然很“傻”:它没有长期记忆,换个会话就忘光;没有任务规划,复杂问题不会自动拆解;没有反思机制,答错了不会自我检查。这些正是我接下来想逐个攻克的。

Day5我计划先做记忆层,把历史消息存进一个存储,并支持按关键词或向量检索后动态拼进上下文。然后会尝试给循环加一个“反思”节点:在模型给最终答案之前,多问一次它“这个答案有没有依据,要不要先调用工具验证”。最后再把一个任务拆成子步骤,逐步执行。

我个人实际操作中很深的体会是:学Agent一定要尽早把“模型调用层”和“执行逻辑层”分清楚。模型再怎么聪明,也只负责出主意;代码再怎么简单,也要负责落地。这个最小Agent就是一个很好的分界线样板。如果你也在自学Agent,建议不要只收藏这篇,而是亲手把代码敲一遍,换几个测试问题试试。等你能徒手写出这个循环,再去看任何框架都只会觉得“原来是这么一回事”。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 11:54:49

电费管理系统Java源码拆解:从跑通到二次开发避坑指南

简介:这是一份面向高校计算机相关专业毕业设计场景的Java电费管理系统完整源码包,适合正在准备毕设或需要Java Web实战练手的学生与开发者。系统围绕居民小区或企业的电费业务展开,涵盖用户管理、电费计算、在线缴费、数据统计与缴费提醒等模…

作者头像 李华
网站建设 2026/10/21 11:53:55

MATLAB实现FDTD二维电磁波传播模拟:从原理到代码实战

FDTD(时域有限差分)这名字听起来挺唬人,但把它拆开看:有限差分,就是把麦克斯韦方程组里那些偏微分换成加减乘除;时域,就是一格一格往前推进时间。用 MATLAB 写一个基于 FDTD 的二维电磁波传播模…

作者头像 李华
网站建设 2026/10/21 11:42:39

【动态规划: 线性dp】解码方法

文章目录题目解析算法原理确定状态表示确定状态转移方程初始化确定填表顺序确定返回值细节问题代码实现普通动态规划优化初始化版本空间优化版本题目链接:91. 解码方法 题目解析 动态规划(Dynamic programming,简称 DP)是一种通过…

作者头像 李华
网站建设 2026/10/21 11:39:26

新手程序员必看:一步步走进大模型背后的知识库世界

本文从关键词检索到Agent,详细介绍了知识库技术的发展脉络和不同阶段的核心能力。文章强调知识系统能力的逐步扩展,而非严格的技术替代顺序,并通过实例说明了如何根据实际问题类型选择合适的技术。理解每种技术所处位置比追逐“最新”技术更重…

作者头像 李华
网站建设 2026/10/21 11:36:20

YOLOv11损失函数改进之FSEIoU:无人机海上救援场景下的自适应边界框回归

一句话结论:在SeaDronesSee海上救援基准上,将YOLO11n的边界框回归损失从CIoU替换为FSEIoU,mAP50提升1.6个百分点、mAP50-95提升0.7个百分点,且不增加任何推理开销。配合FOE-YOLO的轻量化颈部设计,整体模型在GFLOPs降低28.6%的前提下,mAP50和mAP50-95分别比基线YOLO11n高出…

作者头像 李华