学了一周Python,我决定手搓一个最小的AI Agent循环(附源码)

如题,本人是一位刚入坑Python和Agent的小白选手,在花了一周时间学习了Python基础之后,决定自己动手写一个简单的Agent,但实际写下来发现是一件不简单的事,以下是我的心路历程:

第一步先准备好自己LLM的API Key,然后再根据你获取这个key源的官方文档,找到调用的BaseUrl和选择你想要提问的模型。 并将他们写到代码里。

ini 复制代码
#这里我用的是deepseek的官方apikey
URL = "https://api.deepseek.com/chat/completions" 
API_KEY = "换成自己的api_key"
MODEL = "deepseek-v4-flash"

第二步,设置好往大模型发送请求的请求头,把 API Key 放进 Authorization 里。。

css 复制代码
HEADERS = {
    "content-type":"application/json",
    "Authorization":f"Bearer {API_KEY}",
}

第三步,写一个简单的工具函数,供LLM调用。

以最简单的计算器函数为例(其实直接问LLM也可以得出答案,但是Agent本质就是LLM来调用tools):

python 复制代码
def calculate(expression:str)->str:
    result =eval(expression)
    return result

第四步,编写一个工具注册表以及工具声明(一定要把 description 写清楚,描述得越准确,大模型就越能挑对工具、传对参数。)。

ini 复制代码
TOOL_REGISTRY = {
    "calculate": calculate
}

TOOL_SCHEMA = [
    {
        #先声明工具类型为函数   
        "type" : "function" , 
        #对此函数进行说明
        "function" : {  
            #函数名(对标工具注册表注册好的工具类型)
            "name" : "calculate" ,
            #描述这个函数是干什么的,模型收到相关请求时会调用到此工具
            "description" : "计算一个数学表达式" ,
            #声明输入参数
            "parameters" : { 
                 #一般来说声明参数列表都是object类型
                "type" : "object" ,
                #将参数说明列表列出
                #参数名,参数类型,参数描述
                "properties" : { 
                    "expression" : { "type" : "string" , "description" : "数学表达式" } ,
                    } ,
                 #必需参数
                "required" : [ "expression" ] ,
                } ,
            } ,
        } ,
    ]

第五步,将前面这些准备好的组件组合使用。这一步是核心,本质上就是一个Think-Act-Observe(思考-行动-观察) 的循环:

  1. 带着上下文和工具清单去提问大模型;
  2. 如果模型决定调用工具,就执行工具,再把结果喂回去;
  3. 模型拿到工具结果后组织语言作答。

生产环境中复杂的工具和复杂的提问,会让这个循环不止进行两轮。

建议是一步一个print,把大模型返回的原始json和自己对于字典提取的部分的逻辑验证一下:

ini 复制代码
def run_agent(user_input:str)->str:
    messages = [
        {"role":"system","content":"你是一个助手,可以根据用户的请求在需要时调用工具。得到结果后再回答。"},                                                 #设定初始提示词
        {"role":"user","content":user_input},
    ]
    #建立多次循环,因为至少需要两次提问,才能解决问题,也不能循环过多导致token浪费掉
    for _ in range(8):
        #将提问的模型、组装好的提示词、工具说明作为向模型发送请求的负载
        payload = {"model":MODEL,"messages":messages,"tools":TOOL_SCHEMA}  
        #获取发送请求后的响应体
        response = requests.post(URL, headers=HEADERS, data=json.dumps(payload)) 
        #提取模型响应体里我们需要的部分
        reply = response.json()["choices"][0]["message"]  
        #将回答加入到下一次发送请求的提示词中,保留上下文
        messages.append(reply) 
        #提取出模型根据用户的提问内容和已有的工具列表想要去调用的工具
        tool_calls = reply.get("tool_calls") 
        #如果工具列表中没有元素,意味着模型不需要使用工具就能回答问题,直接返回回答
        if not tool_calls:
            return reply["content"] 
        for tc in tool_calls:
            #提取工具函数名
            name = tc["function"]["name"]
            print(f"调用的工具名:{name}")
            #提取模型写好的调用入参
            args = json.loads(tc["function"]["arguments"])
            #执行工具
            result = TOOL_REGISTRY[name](**args)
            #将工具函数的运行结果,加入到上下文中
            messages.append({  
                "role":"tool",
                "tool_call_id":tc["id"],
                "content":str(result),
            })
    return "未计算出答案"

第六步,写一个调用run_agent函数的main方法作为收尾:

ini 复制代码
if __name__ == "__main__":
    question = input()
    print(run_agent(question))

运行结果如下:

由于我的注释写得太烂了,所以我决定让我的ClaudeCode给我优化一下注释,完整的代码如下:

python 复制代码
import requests
import json

# ===================== 基础配置 =====================
URL = "https://api.deepseek.com/chat/completions"
API_KEY = "apikey"
MODEL = "deepseek-v4-flash"

HEADERS = {
    "content-type": "application/json",
    "Authorization": f"Bearer {API_KEY}",
}


# ===================== 工具定义 =====================
def calculate(expression: str) -> str:
    """计算一个数学表达式,返回计算结果。"""
    result = eval(expression)
    return result


# 工具注册表:工具名 -> 实际执行的函数
TOOL_REGISTRY = {
    "calculate": calculate,
}

# 工具描述:提供给模型的工具清单,模型据此决定是否调用及如何传参
TOOL_SCHEMA = [
    {
        "type": "function",
        "function": {
            "name": "calculate",              # 需与工具注册表中的键一致
            "description": "计算一个数学表达式",
            "parameters": {
                "type": "object",
                "properties": {
                    # 参数名: {类型, 描述}
                    "expression": {"type": "string", "description": "数学表达式"},
                },
                "required": ["expression"],   # 必填参数
            },
        },
    },
]


# ===================== 主逻辑 =====================
def run_agent(user_input: str) -> str:
    """运行一轮 Agent 对话:模型可按需调用工具,拿到结果后再作答。"""
    messages = [
        # 系统提示词,设定助手角色
        {"role": "system", "content": "你是一个助手,可以根据用户的请求在需要时调用工具。得到结果后再回答。"},
        {"role": "user", "content": user_input},
    ]

    # 多轮循环:至少两轮(调用工具 + 拿结果作答),上限 8 轮以防 token 浪费
    for _ in range(8):
        # 组装请求负载:模型 + 完整上下文 + 工具清单
        payload = {"model": MODEL, "messages": messages, "tools": TOOL_SCHEMA}
        response = requests.post(URL, headers=HEADERS, data=json.dumps(payload))
        reply = response.json()["choices"][0]["message"]

        # 将模型回复追加进上下文,保留对话历史
        messages.append(reply)

        tool_calls = reply.get("tool_calls")
        # 无工具调用,说明模型已能直接作答
        if not tool_calls:
            return reply["content"]

        # 逐个执行模型请求的工具调用
        for tc in tool_calls:
            name = tc["function"]["name"]
            print(f"调用的工具名:{name}")
            args = json.loads(tc["function"]["arguments"])   # 模型生成的入参
            result = TOOL_REGISTRY[name](**args)             # 执行对应工具

            # 将工具结果回填进上下文,供模型下一轮参考
            messages.append({
                "role": "tool",
                "tool_call_id": tc["id"],
                "content": str(result),
            })

    return "未计算出答案"


if __name__ == "__main__":
    question = input()
    print(run_agent(question))

毕竟是"极简"Agent,本期只是用几十行原生代码,将一个具备核心要素(工具注册、工具描述、思考循环)的 Agent 骨架搭了起来。

现在的这个 Agent 还很脆弱,随便输入点不规范的内容可能就会崩溃。下一篇,我将会分享如何将"防御性编程"运用到此 Agent 的开发中,让它变得真正健壮可用。

相关推荐
程序员秋天7 小时前
用Spring AI实现多轮对话记忆,别再让AI每次都"失忆"
agent·ai编程
大强同学10 小时前
Kimi Code CLI
人工智能·agent·claude·skill·kimi code cli
刘棕霆10 小时前
造数脚本越堆越乱:稳定的沉淀成引擎,变化的留在配置
aigc·agent·测试
带刺的坐椅11 小时前
Solon TeamAgent 协作协议:从 SEQUENTIAL 流水线到 HIERARCHICAL 主管团队
java·ai·llm·agent·solon
用户02071992077211 小时前
别把 Semgrep 当高级 grep:用污点规则抓住命令注入,并把结果送进 CI
agent
我要割麦子11 小时前
从零到一手撸 Agent 系列 — 第 4 篇:工具的契约 — Tool 接口与注册表
agent·ai编程
wWYy.11 小时前
什么是Multi-Agent?
agent
怕浪猫12 小时前
# 第3章 记忆系统:构建Agent的长短期记忆
openai·agent·ai编程