news 2026/9/10 5:12:12

10行代码跑通大模型API:从裸调用到Agent开发避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
10行代码跑通大模型API:从裸调用到Agent开发避坑指南

最近在折腾 Agent 开发,我给自己定了个规矩:不上框架、不碰现成项目,就从最底层的大模型调用开始,用 10 行代码把第一步跑通。这个决定让我少走了不少弯路,但同样也踩了 4 个坑。如果你正在学 Agent、纠结要不要直接上 LangChain 这类框架,这篇文章应该能帮你省下大半天时间。

项目本身不复杂:先建一个干净的 Python 环境,用官方 SDK 调一次大模型 API,让模型回一句话。区别在于,我没有把这次调用当成“hello world”完事,而是把它当成 Agent 的地基来打——每一次请求要传什么、返回什么、哪里容易出错、为什么出错,我都做了记录。跑通之后我再用这 10 行代码延伸出工具调用、多轮记忆,才真正理解了 Agent 的运作逻辑。

这篇文章适合两类人:一是完全没调过大模型 API、想知道第一步怎么迈的纯新手;二是已经用框架写过 Agent、但没试过“裸调用”、想补底层认知的开发者。下面我按照实际操作顺序,把代码拆解、完整流程、以及踩到的 4 个坑全部写出来。

1. 项目定位与设计思路

1.1 为什么要先跑通一次“裸调用”

现在 Agent 开发的学习资源非常多,随便一搜就是 LangChain、CrewAI、MetaGPT、AutoGen,教程上来就教你建 Agent、挂工具、做多智能体协作。好处是上手快,坏处是很多人写了一堆代码,却连“模型到底是怎么被调起来的”都说不清楚。

我的建议是:先跑一次裸调用。所谓裸调用,就是不走任何框架、不做任何封装,直接拿代码请求大模型的 API,拿到模型返回的文本。这一步能验证四件事:

  • 开发环境能不能正常工作;
  • API Key 和网络通道有没有问题;
  • 你选的模型名在平台上是否存在;
  • 你发出去的 messages 格式是否正确。

这四件事任何一个有问题,后面所有 Agent 功能都没法跑。与其将来在框架里被各种抽象层挡住,不如一开始就把变量控制到最少,出问题也知道去哪查。

在裸调用阶段,我不需要考虑记忆、不考虑工具、不考虑 Agent 的“自主决策”,只需要做一件事:让模型收到你的话,并且正确回复你。这个闭环一旦建立,后面所有东西都是在它之上加逻辑。

1.2 技术选型:Python + OpenAI 兼容接口

技术选型我参考了当前行业的主流做法:开发语言选 Python,接口形式选 OpenAI 兼容格式。这里解释一下为什么。

Python 在大模型生态里是事实标准,不管哪个框架、哪个平台,官方 SDK 和示例基本都是 Python 优先。而且 Python 写这类胶水代码非常快,一个文件就能跑起来,不需要处理编译问题。如果你只会 JavaScript/TypeScript,也不是不行,Node.js 同样有官方 SDK,只是后续学习 Agent 框架时,Python 的资料会多出很多。

接口格式选 OpenAI 兼容,是因为它已经成了行业通用协议。现在国内外的模型服务商,绝大多数都提供了兼容 OpenAI 格式的接口,你只需要改 base_url 和 model 名称,代码结构完全不用变。这意味着你的学习成果可以平滑迁移到不同平台,不会被某一家绑定。

项目最终用的核心依赖只有一个openaiPython 包,不需要装任何 Agent 框架。这一点很重要:依赖越少,出问题时的排查范围就越小。

1.3 Agent 的最小闭环是什么

很多人对“Agent”这个概念有误解,以为 Agent 就是“会自己写代码的机器人”,或者“能自动完成复杂任务的 AI”。如果从工程角度看,Agent 的最小闭环其实是四个环节的循环:

  • 感知:接收用户输入或环境信息;
  • 决策:让大模型根据信息决定做什么;
  • 执行:调用工具、执行代码或搜索;
  • 观察:把执行结果反馈给模型,进入下一轮决策。

这四步循环,第一步就是“让大模型能响应你”。所以裸调用不是 Agent 的全部,但它是 Agent 的起点。我在设计这个项目时,心里一直装着这个闭环,才会有意识地往后延伸。

2. 10 行核心代码逐行拆解

2.1 代码全貌

下面这段代码,就是我说的“10 行代码跑通第一次大模型调用”。我删掉空行后数了一下,正好 10 行。

from openai import OpenAI client = OpenAI( api_key="sk-替换成你的Key", base_url="https://api.example.com/v1", ) resp = client.chat.completions.create( model="your-model-name", messages=[{"role": "user", "content": "你好,请用一句话介绍你自己"}], ) print(resp.choices[0].message.content)

这段代码很短,但它是一个完整的、可运行的请求闭环。不要觉得它简单就跳过,后面所有 Agent 的功能都建立在它之上。下面我逐行拆。

2.2 每一行在做什么

第一行from openai import OpenAI是导入官方 SDK。安装命令很简单,pip install openai,如果你的 Python 环境有多个,记得先激活虚拟环境再装。这个包把我们和 API 交互的细节全部封装好了,不用自己拼 HTTP 请求。

第 3 到 5 行是初始化一个客户端对象。api_key是身份凭证,就像你进公司大厦的工牌;base_url是 API 地址,默认指向 OpenAI 官方,但国内很多平台都有兼容接口,你把地址换成平台的网关地址就行。

第 7 到 10 行是真正的请求操作。client.chat.completions.create表示创建一个对话补全请求,传了两个核心参数:

  • model:你要用哪个模型;
  • messages:对话消息列表。

这里的messages是整个 Agent 开发的灵魂。它是个数组,每个元素至少有rolecontent两个字段。role有三种:system表示系统设定,user表示用户输入,assistant表示模型回复。你传入什么,模型就在这个上下文中继续生成。

最后一行print把模型回复打印出来。这里要注意访问路径:resp.choices[0].message.content,先取第一个回复选项,再取里面的消息内容。

2.3 参数选择:temperature、max_tokens、stream

刚跑通时,很多人只传modelmessages,这没问题。但真要用于开发,还有三个参数需要理解。

参数作用我的推荐值说明
temperature控制随机性,0 到 20.7数值越高回答越发散,越低越保守。Agent 做工具调用时可以调低到 0.2 左右,减少中间步骤出错概率
max_tokens限制最大生成 token 数视场景而定不设置可能消耗过多 token,设置太小会截断回复
stream是否流式输出False调试阶段设 False 拿到完整结果,做对话应用再开启流式提升体验

我调试时会把temperature设成 0,因为我不想让模型“发挥”,我需要稳定、可复现的结果。等到做聊天机器人才调回 0.7。参数没有绝对标准,但你要知道每个参数影响什么,否则后面出现问题根本无从排查。

2.4 这段代码离 Agent 还差多远

诚实说,这段代码本身还不是 Agent,它只是一个“对话模型调用”。它没有记忆、没有工具、没有自主决策。但它证明了最关键的事实:模型可以被稳定调起来了。

从这段代码到真正的 Agent,中间还差三样东西:

  • 记忆能力:维护 messages 列表,把历史对话带回去;
  • 工具接口:模型需要调用外部函数时,怎么定义、怎么执行、怎么回填;
  • 循环逻辑:模型决策后,系统执行工具,再把结果送回模型,直到任务完成。

这三个能力我后面都会讲到。我想表达的是:所有复杂系统都建立在这个 10 行代码的调用之上,不要觉得它简单,这是整个 Agent 的“原胞”。

3. 完整实操过程:从环境准备到首次对话

3.1 环境准备

我先说环境,这是很多人忽略但最容易翻车的环节。

我建议用 Python 3.10 以上版本。先建一个虚拟环境,避免污染系统 Python:

python -m venv .venv source .venv/bin/activate

在虚拟环境里安装依赖:

pip install openai python-dotenv

我同时装了一个python-dotenv,它是用来加载环境变量的工具。API Key 这类信息尽量不要硬编码在代码里,否则你把代码传到公开仓库时,Key 就裸奔了。我的做法是项目根目录建一个.env文件:

OPENAI_API_KEY=sk-你的Key OPENAI_BASE_URL=https://api.example.com/v1 MODEL_NAME=your-model-name

然后在 Python 里用dotenv加载:

from dotenv import load_dotenv import os load_dotenv() api_key = os.getenv("OPENAI_API_KEY") base_url = os.getenv("OPENAI_BASE_URL") model_name = os.getenv("MODEL_NAME")

很多平台的 Key 是按用量计费的,一旦泄露被拿去刷量,损失就得自己承担。我习惯在代码里不写任何真实 Key,而是全部从环境变量读取,这是做项目第一课。

3.2 写代码、跑起来

准备好之后,把核心代码放到一个main.py文件里。我第一次运行时,在终端执行:

python main.py

结果等了几秒钟,终端打印出了模型回复。那一刻其实没有太多兴奋,更多的是“哦,通了”。但再回头想,这个“通了”背后包含了很多环节:DNS 解析、TLS 握手、鉴权、请求格式校验、模型推理、响应解析。任何一个环节有问题,都不会有这个输出。

如果你也想复现,我建议把请求超时时间也加上,避免网络卡住导致程序一直挂起:

resp = client.chat.completions.create( model=model_name, messages=[{"role": "user", "content": "你好"}], timeout=30, )

timeout参数在 SDK 里可以直接传,如果 30 秒内没有响应,程序会抛异常,方便你及时发现问题。

3.3 如何判断真的成功了

很多人以为“打印出了内容”就是成功,其实还不够。我建议你打印完整的响应对象,仔细看一次:

print(resp)

完整响应里包含几个关键字段:

  • id:这次请求的唯一标识,排查问题时可以通过它找日志;
  • choices:模型生成的候选列表,这里我们只取第一个;
  • usage:token 使用量,包括输入 token、输出 token、总 token;
  • created:请求创建时间戳。

我会特别关注finish_reason,它在choices[0]里。如果值是stop,说明是正常结束;如果是length,说明 max_tokens 不够,回复被截断了。

平时开发时,我会把响应摘要打到日志里,方便追踪每次调用的 token 消耗。这在大模型应用里不是小事——成本分析全靠它。

3.4 做到“可控”,才算真正会调用

跑通一次调用不算什么,真正会调用是要让这次调用可控。我做的第一件事是加一个简单的错误处理框架:

try: resp = client.chat.completions.create( model=model_name, messages=messages, timeout=30, ) return resp.choices[0].message.content except Exception as e: print(f"请求失败: {e}") return None

大模型 API 本质上是一个远程服务,网络抖动、限流、服务端过载都是常态。写 Agent 代码时,每次模型调用都应该有异常处理。不要觉得多此一举,真正跑到生产环境你就知道了,模型调用是最容易出问题的外部依赖之一。

再往上一层,我还会加“重试”。比如遇到 429(限流)或 5xx(服务端错误),等待几秒再重试一次:

import time for attempt in range(3): try: resp = client.chat.completions.create(...) return resp.choices[0].message.content except Exception as e: if attempt < 2: time.sleep(2 * (attempt + 1)) else: raise

这就是简单重试,足够用。等以后你用到流式输出、函数调用时,再考虑更完善的重试策略。

4. 踩过的 4 个坑,每一个都值得记下来

4.1 坑一:API Key 没生效,报 401

现象很典型:代码完全按照示例写的,但一运行,终端报错:

401 Invalid authentication credentials

我一开始以为是 SDK 版本问题,后来发现根本不是。问题出在 API Key 的配置方式上。我当时的 Key 是直接复制到代码里的,看起来没问题,但仔细对比后发现复制时多了一个空格。

这个坑的真正常见原因有三个:

  • Key 前后有隐藏空格或换行符;
  • 平台开启了 IP 白名单,你当前机器 IP 不在允许列表里;
  • 用了已注销或过期的 Key。

排查方式也简单:先打印出配置的 Key 和长度,确认没有多余字符;再去平台后台确认白名单;最后用一个最简单的请求测试 Key 是否有效。

这里我有个心得:一定要先用一个单独的、极简的 Python 文件验证 Key,不要把它混在业务代码里。我后来每次对接新平台,都是先写一个 5 行的测试脚本,只做一次最小请求,通了再往下写。

4.2 坑二:模型名写错或渠道不支持

第二个坑是模型名。我的代码里写的是:

model="gpt-4"

结果报错:

404 The model does not exist

真实原因是我使用的平台并不提供名为gpt-4的模型,或者说它提供了兼容接口,但模型名是另一种写法。比如有些平台把模型命名为gpt-4-32k,有些则需要带版本后缀gpt-4-0125-preview。模型名少一个后缀、多一个短横线,请求都会失败。

这个坑的排查方法有三种:

  • 查询平台文档中的模型列表;
  • 调用接口主动拉取模型列表,比如client.models.list()
  • 在平台控制台看模型名称说明。

我自己写了一个小脚本,用来列出账号能访问的全部模型:

models = client.models.list() for m in models.data: print(m.id)

这样可以快速确认到底有哪些模型可以用,避免瞎猜。从那以后,我每换一个平台,第一步永远是拉一次模型列表,看一眼再动手。

模型名这件事还有个更深的影响:它会直接影响 Agent 的能力边界。比如有些模型不支持函数调用,你用这种模型做工具调用,怎么调都会失败。所以在选模型时,不仅要看能不能回话,还要确认是否支持工具调用、上下文长度是否够用。

4.3 坑三:把记忆当成了多轮对话

第三个坑,是我跑通单次调用后,想做多轮对话时踩的。

我一开始的理解是:我把第一轮的回复直接拼到messages里再发一次,模型应该就能记得之前聊过什么。结果它确实“记得”了,但一切建立在一种很朴素的方式上——把历史消息原样传回去。

真正的坑在于:很多人没有维护 messages 列表,而是只存了一个字符串变量,把以往的对话用换行拼接到content里再发给模型。这样的问题是系统指令(system)、用户消息、模型回复被揉在一起,模型的上下文格式会混乱,经常出现奇奇怪怪的回答。

正确的做法是,始终维护一个结构化的消息列表:

messages = [ {"role": "system", "content": "你是一个乐于助人的助理。"}, ] while True: user_input = input("我:") messages.append({"role": "user", "content": user_input}) resp = client.chat.completions.create( model=model_name, messages=messages, ) assistant_msg = resp.choices[0].message.content messages.append({"role": "assistant", "content": assistant_msg}) print(f"AI:{assistant_msg}")

每次请求,把整个messages列表传过去,它会作为模型的“记忆”存在。这个列表就是 Agent 记忆的最初级形态。

但这里还有一个隐含的坑:token 会越积越多。当对话达到几十轮后,消息列表会变得很长,成本上升、响应变慢、还可能超出模型的上下文窗口。所以后面必须引入截断策略,比如只保留最近 10 轮对话,或者把早期对话摘要后放进系统提示。

Agent 的记忆远不止“多轮对话”,但多轮对话是记忆的地基。你连结构化消息列表都没建好,后面做 RAG、向量记忆都会很吃力。

4.4 坑四:工具调用结果回填格式不对

第四个坑是我从“纯对话”迈向“Agent”时遇到的,也是我觉得价值最大的一个。

大模型本身不能执行真实操作,它只能“建议”你应该调用某个工具。典型的调用过程是:

  1. 你定义一个函数,比如获取天气的工具;
  2. 你把这个函数的描述、参数格式发给模型;
  3. 模型判断用户需要查天气时,返回一个tool_calls结构,里面包含函数名和参数;
  4. 你的程序执行真实函数拿到结果;
  5. 把结果以role=tool的消息传回给模型;
  6. 模型基于结果生成最终回答。

听起来顺理成章,实际写的时候就容易踩坑。我第一次做的时候,在第 5 步传回格式写错了。返回结果的 messages 没有与 assistant 之前的tool_call_id关联,导致模型报错:

Invalid parameter: messages with role 'tool' must be a response to a preceding message with 'tool_calls'.

这个报错信息翻译过来就是:你传的 tool 结果没有对应上一次 assistant 消息里的 tool_calls 请求。

正确的做法是:当模型返回tool_calls时,你把这一整条 assistant 消息原样加入messages,然后每个工具执行结果都用role=tool单独一条消息加入,且每条都要带上对应的tool_call_id。官方的标准序列大概是:

# 加入 assistant 的 tool_calls 消息 messages.append(resp.choices[0].message) # 加入工具执行结果 for tool_call in resp.choices[0].message.tool_calls: result = run_tool(tool_call.function.name, tool_call.function.arguments) messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": str(result), })

然后再次调用模型,模型会结合工具结果给出最终回复。

这个坑之所以重要,是因为它就是 Agent 和普通聊天机器人的分水岭。Agent 的“行动能力”完全依赖这条调用链:模型给指令,程序执行,回填结果,模型再决策。链条任何一个环节的格式不对,整个循环就断了。

我当时调试了很久,后来是把模型返回的完整消息体打印出来,一行一行对比官方规范,才找到问题。所以遇到工具调用相关报错,先打印原始响应,别靠猜。

5. 从第一次调用到真正的 Agent

5.1 Agent 的最小闭环

跑通前面的流程后,我对 Agent 的认识终于从概念变成了具体代码。这里以“查天气”为例,演示一下怎么从 10 行代码出发,拼一个最小 Agent。

第一步,定义一个工具函数。因为手边没有真实天气 API,我用一个假函数模拟:

def get_weather(city: str): # 真实项目里这里会请求天气服务 weather_map = { "北京": "晴,25度", "上海": "小雨,22度", "广州": "多云,28度", } return weather_map.get(city, "暂无数据")

第二步,告诉模型有这个工具。这是在messages之外传一个tools参数,用 JSON Schema 描述函数:

tools = [ { "type": "function", "function": { "name": "get_weather", "description": "查询指定城市的天气", "parameters": { "type": "object", "properties": { "city": {"type": "string", "description": "城市名"} }, "required": ["city"], }, }, } ]

然后跑循环。用户说“北京天气怎么样”,模型判断要查天气,返回tool_calls,程序执行函数,拿到结果后回填,再让模型组织语言回复。这个循环跑起来,你手上就是一个“会调用工具”的最小 Agent 了。

我没有在这里放出完整循环代码,因为完整循环需要处理流式、并发等细节。但核心思路你已经清楚了:工具的注册、执行结果的回填,就是前面 4.4 讲的坑。把那个坑填平,Agent 的行动能力就打通了。

5.2 框架用不用?什么时候用

我觉得这是 Agent 开发里最值得说的问题。我的建议是:先裸写,再用框架。

裸写的价值在于,你知道每一次请求背后发生了什么。等你理解了工具调用、记忆、循环这些概念,再上手 LangChain、CrewAI 这类框架,就不会有“黑盒恐惧”。框架能帮你省掉大量重复代码,比如多 Agent 编排、Prompt 模板、任务分解。但如果你没有底层认知,框架封装得越漂亮,你越难排查问题。

我个人的时间线是:先花两天时间裸写,从调用到工具、到多轮对话;然后开始用框架重写同一个示例,对比自己的代码和框架的差异;最后再决定哪些场景直接用框架、哪些场景保持裸写。

很多 Agent 项目的失败不是框架不好,而是使用者根本不理解模型调用机制,遇到一个报错就卡死。所以动手写框架前,先把裸调用这步练扎实。

5.3 接下来可以怎么学

如果你跟着这篇文章跑通了第一次调用,也理解了那 4 个坑,下一步的学习路线我建议这样走:

  • 第一步,把单次调用封装成函数,支持多轮对话;
  • 第二步,实现一个简单的工具调用循环,让模型能调用你写的函数;
  • 第三步,给 Agent 增加长期记忆,比如用向量数据库保存历史信息;
  • 第四步,拆分多个 Agent,让它们通过消息协作完成任务;
  • 第五步,这时候再回头看框架,你会理解和吸收得很快。

我在实际做的时候,一直保持着一个习惯:每引入一个新概念,就回到那 10 行代码问自己一句,这个问题是不是改一下 messages 或参数就能解决。这个习惯让我的学习路径变得特别清晰。

拿工具调用来说,它并不是一套全新的魔法,本质上只是让模型输出的结构化格式,由普通文本变成了tool_callsJSON。理解了这一点,Agent 的“自主决策”就没什么神秘的了——无非是循环里根据模型输出类型做不同的分支处理。

聊到这儿,这次“从零手撸 Agent”的核心内容就全部讲完了。如果你也想动手做,我建议不要纠结框架和工具链,先打开编辑器,把第 2 节那段代码跑通。只有跑通一次,你才算真正跨进了 Agent 开发的门。后面那 4 个坑,你大概率也会遇到,但我希望你看完这篇文章后,能少花一点时间在排错上,把这些时间留给真正的设计思考。

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

Ubuntu 20.04 是无人机飞控开发的系统性基石

1. 这不是装个系统那么简单&#xff1a;为什么无人机飞控开发必须从 Ubuntu 20.04 开始筑基你手头有一块 Pixhawk 飞控板&#xff0c;刚焊好电机线&#xff0c;连上电脑——串口灯亮了&#xff0c;但 QGroundControl 里却只显示“连接中…”&#xff0c;等三分钟没反应&#xf…

作者头像 李华
网站建设 2026/9/10 5:11:23

近红外光谱PLS定量分析建模:从数据预处理到模型部署全指南

简介&#xff1a;面向近红外光谱分析与化学计量学入门者&#xff0c;这份资源聚焦偏最小二乘法&#xff08;PLS&#xff09;建模的完整流程&#xff0c;也适用于食品、制药、农业等领域的光谱数据分析人员。近红外光谱中每个波长响应可视为自变量&#xff0c;目标物理或化学性质…

作者头像 李华
网站建设 2026/9/10 5:09:50

CANN/ge ArgDescInfo构造函数

ArgDescInfo构造函数和析构函数 【免费下载链接】ge GE&#xff08;Graph Engine&#xff09;是面向昇腾的图编译器和执行器&#xff0c;提供了计算图优化、多流并行、内存复用和模型下沉等技术手段&#xff0c;加速模型执行效率&#xff0c;减少模型内存占用。 GE 提供对 PyTo…

作者头像 李华
网站建设 2026/9/10 5:07:49

无人机开发环境搭建指南:Ubuntu 20.04、ROS与PX4实战

1. 为什么无人机开发绕不开 Ubuntu 20.041.1 开源飞控生态的现实&#xff1a;你在和什么打交道做无人机软件开发&#xff0c;特别是接触开源自驾仪&#xff08;PX4、ArduPilot&#xff09;或者相关的机载计算机、视觉导航、ROS 集群这类方向&#xff0c;Linux 基本不是“选项”…

作者头像 李华
网站建设 2026/9/10 5:07:39

OV7670 SCCB配置详解:FPGA主机实现与常见调试问题排除

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华