惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

V
Vulnerabilities – Threatpost
aimingoo的专栏
aimingoo的专栏
B
Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
GbyAI
GbyAI
阮一峰的网络日志
阮一峰的网络日志
Engineering at Meta
Engineering at Meta
IT之家
IT之家
V
Visual Studio Blog
The Cloudflare Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
A
About on SuperTechFans
博客园 - 聂微东
Blog — PlanetScale
Blog — PlanetScale
N
News and Events Feed by Topic
A
Arctic Wolf
WordPress大学
WordPress大学
小众软件
小众软件
C
CERT Recently Published Vulnerability Notes
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
D
Darknet – Hacking Tools, Hacker News & Cyber Security
F
Fortinet All Blogs
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Y
Y Combinator Blog
T
Threat Research - Cisco Blogs
Latest news
Latest news
Simon Willison's Weblog
Simon Willison's Weblog
Cyberwarzone
Cyberwarzone
S
Schneier on Security
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
L
Lohrmann on Cybersecurity
Stack Overflow Blog
Stack Overflow Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
P
Privacy International News Feed
J
Java Code Geeks
Spread Privacy
Spread Privacy
宝玉的分享
宝玉的分享
I
Intezer
L
LangChain Blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
G
GRAHAM CLULEY
博客园 - 叶小钗
博客园 - 三生石上(FineUI控件)
The GitHub Blog
The GitHub Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
N
News and Events Feed by Topic
AWS News Blog
AWS News Blog
Attack and Defense Labs
Attack and Defense Labs
Security Archives - TechRepublic
Security Archives - TechRepublic
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO

博客园 - 一名程序媛呀

Anki插件开发必知必会:钩子函数与右键菜单定制 httpx 传参总报错?这次把 GET、POST、文件上传到响应处理的坑给你一次填平 从卡顿到丝滑:FastAPI 调用外部 API 的正确姿势(httpx 实战) 还在 XHR、Fetch 和 Axios 之间纠结?我踩过的坑,希望你一个都不用碰到 你的REST接口还在“过度投喂”数据吗?——FastAPI + GraphQL实战避坑指南 Uvicorn、Gunicorn 傻傻分不清?FastAPI 生产部署避坑指南 Termux里的二进制和脚本,到底怎么运行才不踩坑?Termux-service 保活妙招! 刚部署的 LibreTranslate 频频翻车?我掏出了 20 年前的 StarDict 词典,用 FastAPI 搭了个本地词典翻译 API 别再用网页翻译看源码了!你的私人翻译神器LibreTranslate,部署避坑指南来了 掏出手机就能搭个 WebDAV 同步服务器?这操作有点香 别只盯着GitBook了!这个文档神器让你的笔记秒变网站 写爬虫时用了代理还被封?Python 代理的那些隐藏坑,我替你踩明白了 FastAPI 身份验证总踩坑?这份 FastAPI Users “避坑指南”请收好 旧手机别扔!用 Termux 搭个私人云盘,比网盘香多了 你的FastAPI又在服务器上“跑不起来”了?来,今天咱把打包这件事彻底聊透 写页面时别再把 Element Plus 整个搬进来啦!Vue3按需加载的坑我帮你踩平了 前端包管理咋选?我从npm叛逃到pnpm的血泪史(附避坑指南) 聊聊 fetch 使用中我踩过的那些坑和正确打开方式 FastApiAdmin 后端接口开发好了,前端管理界面怎么调用与显示? 给 FastApiAdmin 加个“会议纪要”模块,我把后端二次开发的坑踩了个遍 我用了FastApiAdmin后,连夜把踩过的坑都整理出来了 告别 Typora 后的新欢:我把所有笔记迁移到了 Obsidian 这个“第二大脑” 你的Agent API还在裸奔?从认证到沙箱,我用FastAPI搭了几道防线 让 FastAPI Agent 思考不阻塞:手把手教你实现异步任务与后台处理方案 让FastAPI Agent真正记住你:聊聊会话记忆与持久化存储的落地实践 初探:用 FastAPI 搭建你的第一个 AI Agent 接口 FastAPI 少有人提的实用技巧:把 Depends 依赖提到路由层,代码少写60% FastAPI 生产环境静态文件完全指南:从 /favicon.ico 404 到 HSTS 混合内容,一次全根治 用了loguru我才明白,Python日志还能这么写 FastAPI 后台任务:BackgroundTasks 的使用场景与注意事项 FastAPI配置管理避坑指南:从硬编码到 .env 与 pydantic_settings 类,连路由用法都给你捋清楚 FastAPI 文件上传避坑全指南:分块存盘、类型校验与安全兜底 FastAPI + Pydantic 模型终极实战手册:从能跑就行到固若金汤,这些技巧你一定用得上 FastAPI + SQLAlchemy 2.0 通用CRUD操作手册 —— 从同步到异步,一次讲透 FastAPI订单防超卖实战:从数据库锁到Saga分布式事务,这一篇给你理清了 FastAPI 生产环境避坑指南:用 Alembic 管理数据库迁移,别再手动改表结构了! FastAPI服务半夜又挂了?先别急着重启,查查你的数据库连接池“池子”是不是漏了 FastAPI数据库ORM怎么选?我肝了三个Demo后,终于不再纠结了 Vue 3 组件通信,别只会用 Props 和 Emits 了,这几个狠活儿你得看看 Vue 3 组合式 API 香是香,但从Vue2迁移时你可别像我当初一样踩进这 3 个深坑里 我用fastapi-scaff搭了个项目,两天工期缩到两小时,老板以为我开挂了 FastAPI+Vue:文件分片上传+秒传+断点续传,这坑我帮你踩平了! FastAPI自动生成的API文档太丑?我花了一晚上把它改成了客户愿意付费的样子 告别手写 API 胶水代码:FastAPI 与 Vue 的“契约自动机” OpenAPI 实战 FastAPI + Vue 前后端分离实战:我的项目结构“避坑指南” FastAPI + Celery 实战:异步任务里调用 Redis 和数据库的全解析,及生产级组织方案 FastAPI里玩转Redis和数据库的正确姿势,别让异步任务把你坑哭了! FastAPI + Celery 实战:异步任务的坑与解法,我帮你踩了一遍 FastAPI子应用挂载:别再让root_path坑你一夜 FastAPI项目半夜报警吵醒你?聊聊告警这事儿怎么搞! 别再数据线了!用FastAPI 5分钟搭个局域网文件+剪贴板神器 FastAPI单元测试实战:别等上线被喷才后悔,TestClient用对了真香! FastAPI状态共享秘籍:别再让中间件、依赖和路由“各自为政”了! FastAPI实战:WebSocket vs Socket.IO,这回真给我整明白了! 从0到1,FastAPI + PostgreSQL + Tortoise ORM 实战避坑指南 FastAPI + PostgreSQL 实战:给应用装上“缓存”和“日志”翅膀 FastAPI + PostgreSQL 实战:从入门到不踩坑,一次讲透
FastAPI Agent 函数调用实战:我让 AI 学会了“自己动手查天气“
一名程序媛呀 · 2026-05-10 · via 博客园 - 一名程序媛呀

接上回,我们已经可以通过API接口与Ollama大模型进行对话了,但它的回答基于模型内的训练数据,怎么让他获取实时数据呢?今天来解决这个问题:

你有没有遇到过这种尴尬:辛辛苦苦搭好的 AI 接口,问它“厦门今天多少度”,它一本正经地给你编了个 26°C,还附赠一段虚构的湿度描述。你说它错吧,语气还挺像那么回事;你说它对,它是真的在胡说。

这就是典型的“空脑子”问题——大模型如果没有手,就只能靠记忆瞎掰。今天咱们就给 Agent 装上双手,让它能自己查天气、算数字、搜资料。

全程用 FastAPI + Pydantic + 函数调用,不套重型框架,手动实现一个清晰可控的工具执行模式。

🎯 本文能帮你解决的

把只能“纯聊天”的 Agent 升级成 能调用真实函数干活的智能助手,并且建立一套结构化的工具定义、选择、执行和回退机制。

你会得到一个可扩展的工具链骨架,往后加新功能就像插积木一样顺手。

🧭 核心脉络

🚩 案例引子:一个瞎编天气的 Agent 有多坑

🚩 工具定义的艺术:用 Pydantic 给函数写“说明书”

🚩 意图解析与工具选择:LLM 吐出调用指令后怎么做

🚩 异步执行器 + 错误降级:超时、参数错、网络断都不慌

🚩 实战端点改造:从 /chat/agent,只多几十行代码

🍳 第一部分:空脑子 Agent 的翻车现场

上篇我们把 Ollama 接进了 FastAPI,一个 /chat 端点走天下。

但如果“你问他厦门明天什么天气,他回得有鼻子有眼,但一查根本没那回事。”

这太正常了,因为 模型没有调用外部工具的能力,就像把厨师锁在没有食材的厨房里,只能给你画菜。

解决办法就是 函数调用(Function Calling)
告诉模型:嘿,你没这本事就别硬编了,遇到这类问题,直接告诉我要调哪个工具,我帮你去跑。

🔧 第二部分:核心原理——把函数变成模型能看懂的菜单

整个思路特别像给大厨配一本标准化菜谱:

📋 菜谱(工具定义) → 用 Pydantic 模型描述函数名、作用、参数类型

🧠 大厨看单(意图解析) → LLM 读到用户消息,返回想调用的函数名和参数

👩‍🍳 真正炒菜(执行器) → 我们的代码接收指令,去调用真实的天气 API 或计算函数

🍽️ 上菜(回传结果) → 把工具返回的数据再喂给模型,生成最终的自然语言回答

好,咱们先来定义工具。在 FastAPI 里,我习惯用一个 Pydantic BaseModel 来描述每个工具的输入参数。比如天气查询:

from pydantic import BaseModel

class WeatherParams(BaseModel):
    city: str
    date: str = "today"

然后把工具元信息统一注册到一个列表里。这里注意,尽量别把所有函数都塞进一个散装 dict,参数一多就乱套,调试到凌晨三点才发现是因为忘传了 required 字段。下面给出两种定义方式,作为对比参考。

另外 特别提醒:工具元信息注册格式一定要规范,否则大模型识别不到你的工具函数。
再就是 确保你拉取的模型支持工具函数调用,前面我用的 qwen3:0.6b 结果就是不调用工具,换成 qwen3.5:0.8b 立马识别到工具了,那个心酸呀!
后面多轮对话时,又出现第二轮和第一轮回复相同内容的情况,一通折腾,最后找到原因是小模型做不了轮对话,至少要是 7b 以上!!!吃了电脑配置不够的亏!!!😭

TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "查询指定城市天气",
            "parameters": WeatherParams.model_json_schema()
        }
    },
    {
        "type": "function",
        "function": {
            "name": "calculate",
            "description": "执行数学计算",
            "parameters": {
                "type": "object",
                "properties": {
                    "expression": {"type": "string", "description": "数学表达式"}
                },
                "required": ["expression"]
            }
        }
    }
]

⚡ 第三部分:实战——让 /agent 端点“长出双手”

接下来咱们改造之前的 /chat,变成 /agent

核心变化是:把用户的请求先发给大模型,但这次要多传一个 tools 参数,告诉它你有这些家伙可以用。
模型会返回一个类似 {"name":"get_weather","arguments":{"city":"杭州"}} 的指令。

我在 main.py 里新增了一个异步函数专门处理这件事:

async def execute_tool(tool_name: str, arguments: dict):
    if tool_name == "get_weather":
        # 这里接你们实际的天气 API,我暂时用模拟数据
        city = arguments.get("city", "未知")
        return f"{city}今天晴朗,26.8°C,适合写代码"
    elif tool_name == "calculate":
        expr = arguments.get("expression", "")
        try:
            result = eval(expr)  # 仅演示,生产务必沙箱
            return str(result)
        except Exception:
            return "计算出错,请检查表达式"
    else:
        return "这个工具我还没学会"

你可能会问:“怎么把工具执行结果再送回模型?”
这里有一个我踩了无数坑的细节:一定要在第二轮请求里把之前的消息和工具结果都拼进去,组成完整的对话历史,不然模型会失忆,以为你在自言自语。

端点的写法大致是这样:

@router.post("/agent")
async def agent_endpoint(req: ChatRequest, config: Settings = Depends(get_settings)):
    # 第一轮:发给 LLM,带上 tools
    first_resp = await call_llm(req.message, tools=TOOLS, config=config)
    logger.debug(f"第一轮回复:\n{first_resp }")

    # 如果没有 tool_calls,直接返回内容
    tool_call = first_resp.get("tool_calls", [None])[0]
    if not tool_call:
        return ChatResponse(reply=first_resp["content"])

    # ---- 第二轮:执行工具并拼接消息 ----
    # 1. 把第一轮的 assistant 消息加入历史
    messages = [
        {"role": "system", "content": "你是一个能调用工具的助手,必要时使用工具,否则直接回答。当有工具返回结果时,请直接基于结果回答用户,不要再次调用工具,也不要忽略结果"},
        {"role": "user", "content": req.message},
        first_resp   # assistant 消息,包含 tool_calls
    ]

    # 2. 执行每个工具调用,并将结果作为 tool 消息追加
    for tc in first_resp["tool_calls"]:
        tool_name = tc["function"]["name"]

        # arguments 返回的可能是字串,也可能已经解析为对象了,作个判断
        raw_args = tc["function"]["arguments"]
        if isinstance(raw_args, str):
            arguments = json.loads(raw_args)
        else:
            arguments = raw_args  # 已经是 dict 了,直接用

        result = await execute_tool(tool_name, arguments)   # 你的工具执行器

        messages.append({
            "role": "tool",
            "tool_call_id": tc["id"],   # 必须和上面对齐
            "content": result           # 工具返回的字符串
        })

    # 3. 把完整历史再发给模型,让它总结成人话,不用带 tools 了
    final_resp = await call_llm(messages=messages, config=config)
    logger.debug(f"第二轮回复:\n{first_resp }")
    return ChatResponse(reply=final_resp["content"])

再说个容易翻车的点:工具执行那一步一定要加超时控制和 try-except

之前有次接的天气 API 抽风,整个 /agent 接口跟着一起卡死,前端直接白屏。
后来强制设了 httpx 的超时,并且加了一条黄金规则——工具调用失败时,绝不抛异常,而是把错误信息当作工具结果返回给模型,让它自己圆场。这样用户至少能收到一句“天气服务暂时不可用”,而不是一个冷冰冰的 500。

大模型访问 call_llm() 也要做出重构

核心思路: 用 httpx.AsyncClient 调 Ollama 的 /api/chat 端点(注意是 /api/chat,支持 tools 参数,不是之前的 /api/generate)。在请求体里带上 messages 和 tools,再处理返回的 message 字段。

async def call_llm(
    user_message: str = None,          # 方便快捷调用
    messages: list[dict] = None,       # 传完整历史
    tools: list[dict] = None,
    config: Settings = None,
    system_prompt: str = "你是一个能调用工具的助手,必要时使用工具,否则直接回答。"
):
    if messages is None:
        messages = [{"role": "system", "content": system_prompt}]
        if user_message:
            messages.append({"role": "user", "content": user_message})
    # 也可以选择动态添加 system prompt,但要避免重复,这里简化处理
    logger.debug(f"对话消息:\n{ messages }")

    payload = {
        "model": config.model_name,
        "messages": messages,
        "stream": False
    }
    if tools:
        payload["tools"] = tools

    async with httpx.AsyncClient() as client:
        resp = await client.post(
            f"{config.ollama_base_url}/api/chat",
            json=payload,
            timeout=30.0
        )
        resp.raise_for_status()
        data = resp.json()
        return data["message"]  # 包含 content 和可能的 tool_calls

⚠️ 第四部分:进阶与避坑指南

⭕ 工具定义尽量用 Pydantic 的 schema() 生成,别手写 JSON,那种参数名拼错的疼我挨过太多次。

⭕ 执行器里 eval 只用来演示,生产环境请用安全的表达式解析库,或者直接写死允许的操作。

⭕ 如果 LLM 不支持原生 tool_calls,也可以自己在 system prompt 里要求它返回特定格式的 JSON,然后手动解析,效果一样稳。

⭕ 后续可以把工具执行器改成插件式注册,用装饰器收集,加新工具就跟往工具箱里丢一把扳手一样简单。


今天的更新就到这儿。把一个只会聊天的模型,变成能动手干活的 Agent,那种感觉就像教孩子学会了骑自行车。

如果你也正在这条路上摸索,赶紧把代码拿去跑一跑,遇到坑了别怕,评论区甩过来,咱俩一起填。

觉得有收获的话,点赞、收藏加关注,别让这些实战经验被算法埋没了。下篇我们继续往 Agent 里塞更多东西,不见不散🚀。