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

推荐订阅源

U
Unit 42
N
News and Events Feed by Topic
S
Schneier on Security
G
GRAHAM CLULEY
Scott Helme
Scott Helme
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
GbyAI
GbyAI
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
C
CERT Recently Published Vulnerability Notes
T
The Exploit Database - CXSecurity.com
C
Cisco Blogs
T
The Blog of Author Tim Ferriss
Cisco Talos Blog
Cisco Talos Blog
P
Privacy & Cybersecurity Law Blog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
博客园 - 司徒正美
Blog — PlanetScale
Blog — PlanetScale
Project Zero
Project Zero
MyScale Blog
MyScale Blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Apple Machine Learning Research
Apple Machine Learning Research
小众软件
小众软件
The Last Watchdog
The Last Watchdog
Vercel News
Vercel News
The Cloudflare Blog
C
Check Point Blog
Help Net Security
Help Net Security
Microsoft Security Blog
Microsoft Security Blog
AI
AI
Simon Willison's Weblog
Simon Willison's Weblog
云风的 BLOG
云风的 BLOG
M
MIT News - Artificial intelligence
Stack Overflow Blog
Stack Overflow Blog
腾讯CDC
NISL@THU
NISL@THU
S
Security @ Cisco Blogs
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
S
SegmentFault 最新的问题
MongoDB | Blog
MongoDB | Blog
C
CXSECURITY Database RSS Feed - CXSecurity.com
T
Threatpost
AWS News Blog
AWS News Blog
Cloudbric
Cloudbric
N
News and Events Feed by Topic
PCI Perspectives
PCI Perspectives
S
Securelist
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
V
Vulnerabilities – Threatpost
S
Secure Thoughts

博客园_首页

Plist 二进制格式 Milvus 和 PGVector,哪个更好? OpenClaw 已过时?在 VS Code 中运行 Hermes Agent! 第30篇文章:一个大三计科生的自白 Manim如何在数学公式中完美显示中文? Docker 部署 RocketMQ 5 并发编程核心概念辨析 C#事务处理最佳实践:别再让“主表存了、明细丢了”的破事发生 CLI 是什么?为什么大厂突然集体卷命令行? 【从0到1构建一个ClaudeAgent】协作-自主Agent UIImageView 设置图片不生效的原因排查 最小二乘问题详解20:无先验约束下的增量式SFM自由网平差 痞子衡嵌入式:大话双核i.MXRT1180之XIP应用里借助MU实现可靠Flash IAP的方法 AI Chat 封装, SemanticKerne.AiProvider.Unified 已发布 Windows下右键编辑js文件无法打开记事本——在注册表中使用环境变量 在后台服务中使用 Scoped 服务,为什么总是报错? H200 安装驱动并使用sglang启动模型 wireshark 抓包Trap上报告警内容 我用 AI 辅助开发了一系列小工具(2):图片压缩工具 [A Primer On MC and CC] 2.1 Memory Consistency 1 - 指令重排序和 SC 模型 Oracle数据库SCN推进技术详解与实践指南 玩转控件:封装个带图片的Label控件 Claude Code 4.7 真正该升级的不是模型,而是你的工作流 前端小白一句话,AI 帮我做了个颜值拉满的桌面媒体播放器。当代码不再是门槛,一句话编程就是现实。 5. WorkBuddy: 小龙虾的灵魂三件套,让你的小龙虾不只是工具 SQLite 分片方案实战:三种分片策略的深度对比 告别简陋 UI!一款基于 Fluent Design 和基于 WinUI 的开源免费、现代化的 Avalonia UI 控件库 关于二进制排列组合枚举的总结 AI开发-python-LangGraph框架(3-27-LangGraph从零实现大模型智能决策工作流) ElasticSearch主分片和副本分片概念详解 【002】HTTPS 粗解:证书、TLS 握手与对后端配置的影响 Hermes Agent 一周暴涨五万 Star,但我劝你别急着追 明明连接的是Redis的DB0,为什么能查到DB3的数据? 【从0到1构建一个ClaudeAgent】协作-Agent团队 熟悉电子元器件之后,电子小白下一步该怎么走? MAF快速入门(23)通过C#类定义Skills .NET 高级开发 | 手写一个对象映射框架 FastAPI数据库ORM怎么选?我肝了三个Demo后,终于不再纠结了 mysqldump 参数拾遗:在遗忘与铭记之间 C# .NET 周刊|2026年3月5期 Claude code入门 - 陈彦斌 一文学习入门 ThingsBoard 开源物联网平台 GitHub 热门项目 | 2026年04月16日 如何为GIT设置全局勾子,为每次提交追加信息 Number.isFinite和isFinite与isNaN()和Number.isNaN的区别 PortSwigger SQL注入LAB2 推荐一个测试人必备的Skills,从功能到性能全搞定(附详细实操和安装下载方式) 筑基期:掌握Odoo基础核心知识点02(Odoo XML 开发方式详解) GLM模型这么火,咱们用vllm也咧一个呗! 深入理解 AbortController:从底层原理到跨语言设计哲学 字符串学习笔记 多租户系统框架的基础模块设计和分析设计 Apache SeaTunnel Zeta 为什么能做到“又快又稳”? AI开发-python-LangGraph框架(3-26-LangGraph基本概念及第一个简单样例) Vue 3 组件通信,别只会用 Props 和 Emits 了,这几个狠活儿你得看看 ElasticSearch7.X版本配置密码 用Manim实现动态交点计算--从一个动点问题说起 团结引擎+Addressable+Instant Game打包抖音小游戏 function call 实战:让 LLM 自动判断 pod 异常、调用日志工具并完成故障分析 bubseek —— 让 Agent 的足迹,变成团队的洞察 通过 C# 读取并导出 PDF 书签 如何用 GitHub Actions 实现 Steam 自动化发布 【从0到1构建一个ClaudeAgent】并发-后台任务 .NET 高级开发 | 定制 ASP.NET Core 框架 电子小白:什么是运算放大器(运放) zero2Agent:面向大厂面试的 Agent 工程教程,从概念到生产的完整学习路线 堆上的ORW HC32F460 USB CDC通信异常:非对齐访问异常排查 20260413-Hyperbridge 攻击事件:发生在默克尔山上的验证绕过 那些喊着AI 要淘汰你的人,正在靠你的焦虑赚大钱! 深度学习进阶(八)Swin Transformer 最小二乘问题详解19:带先验约束的增量式SFM优化与实现 SnapTranslate 3.0 正式发布:全局划词翻译 + 完整英语学习闭环,一站式搞定查词、记词、复习 工作的意义、工作的困难认知再思考 .NET + AI 进阶实战:基于类的技能开发 - 打造可治理的 Agent 能力模块 【从0到1构建一个ClaudeAgent】规划与协调-技能 上周热点回顾(4.6-4.12) 电子小白的工具三件套:面包板、杜邦线、万能板 单表五亿数据的查询优化 | Mysql、StarRocks 2. WorkBuddy:从“我是谁”到“帮我干活” C# 如何减少代码运行时间:7 个实战技巧 基于HelixToolkit.SharpDX 渲染3D模型 - 笺上知微 从零开始的双臂具身VLA起源及现阶段发展综述 - SkyXZ 记对 xonsh shell 的使用, 脚本编写, 迁移及调优 - pluvium27 受够了Vibe Coding的失控?换个起点,让AI事半功倍 从开始配置漏洞环境到漏洞复现流程 - 難しい 关于10年工作经验的程序员对OpenClaw的实战经验分享以及看法 - 虚无境 Any metadata 的内存布局 C# .NET 周刊|2026年3月2期 - InCerry 我帮你测过了,测试圈排名第二的 Skill 依然很牛逼 Skill Discovery | 无监督技能发现的经典工作总结 - MoonOut 上下文工程是什么?过时了么?一文讲明白! - 一枫说码 开了 TUN 模式还是直连?90% 的人都踩过这个坑 AScript扩展多种脚本语言 - rockey627 AI 学习笔记:Agent 的记忆机制 你能被装进一个文件里吗?——7 万人把同事"蒸馏"成了 AI - 我没有三颗心脏 Claude Code 通关手册(七):给 AI 装上技能包——Skills 完全指南 - 暮色之狐 在浏览器中快速编辑代码:VSCode Web 集成实践 - Newbe36524 蒸馏自己 skill?基于 Deepseek 的蒸馏器,丐版蒸馏方式,简单便捷 - To_Carpe_Diem Spring AI Aliababa和AgentScope,哪个更好? - 苏三说技术
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 里塞更多东西,不见不散🚀。