Skip to content

函数调用

模型自己不会读本地文件,也不会查数据库。它能做的是看完问题以后,告诉 Python:这里需要调用哪个函数,参数是什么。

执行动作的还是 Python。

打个比方,模型像旁边帮忙判断的人。它可以说“要查一下张三的邮箱”,但联系人文件在本地,打开文件、查名字、返回结果的是 Python。这个关系如果没分清楚,函数调用很容易被想成“模型自动拥有工具”,实际不是。

一、普通调用的问题

如果不接工具,只把问题直接发给模型:

text
帮我看看张三的邮箱是多少

模型看不到本地联系人列表,只能猜,或者说不知道。联系人数据如果在 contacts.json 里,Python 必须先查出来,再交给模型组织回答。

可以直接把整个联系人列表塞进 input,但联系人一多就很难看,也不适合处理更大的资料。函数调用的思路是:先告诉模型有哪些函数能用;模型判断缺资料时,只返回一次函数调用请求;Python 执行函数,再把结果交回模型。

这一套绕一点,但边界清楚:模型负责判断,Python 负责动作。

二、联系人文件

先准备一个很小的联系人文件。

新增文件:contacts.json

json
[
  {
    "name": "张三",
    "email": "zhangsan@example.com",
    "phone": "13800000001"
  },
  {
    "name": "李四",
    "email": "lisi@example.com",
    "phone": "13800000002"
  }
]

先写一个普通 Python 函数,不接模型。

新增文件:scripts/contact_tools.py

python
#!/usr/bin/env python3
"""联系人查询工具函数。"""

import json
from pathlib import Path


def find_contact(name):
    """按姓名查联系人,返回字典;找不到时返回错误信息。"""
    contacts = json.loads(Path("contacts.json").read_text(encoding="utf-8"))

    for contact in contacts:
        if contact["name"] == name:
            return {"ok": True, "contact": contact}

    return {"ok": False, "error": f"联系人不存在: {name}"}


if __name__ == "__main__":
    print(find_contact("张三"))

运行:

bash
uv run python scripts/contact_tools.py

输出类似:

text
{'ok': True, 'contact': {'name': '张三', 'email': 'zhangsan@example.com', 'phone': '13800000001'}}

这一步很普通,但很重要。工具函数本身要先能单独跑通,再接模型。不然函数调用失败时,会分不清是模型参数错了,还是 Python 函数本来就坏了。

三、工具定义

接下来把这个函数描述给模型。工具定义里最关键的是三样东西:函数名、用途说明、参数结构。

python
CONTACT_TOOL = {
    "type": "function",
    "name": "find_contact",
    "description": "按姓名查询联系人信息,返回邮箱和手机号。",
    "parameters": {
        "type": "object",
        "properties": {
            "name": {
                "type": "string",
                "description": "联系人姓名,例如张三",
            }
        },
        "required": ["name"],
        "additionalProperties": False,
    },
    "strict": True,
}

name 要和 Python 函数名能对应上。description 写给模型看,帮助它判断什么时候该用这个函数。parameters 是 JSON Schema,说明参数长什么样。

strict=True 表示按 schema 严格约束参数。这里要求只有一个字符串字段 name,不允许模型额外塞别的字段。参数越清楚,Python 这边越少做猜测。

四、第一次请求

新增文件:scripts/contact_agent.py

先放环境变量检查、联系人函数和工具定义。

python
#!/usr/bin/env python3
"""用函数调用查询联系人。"""

import json
import os
import sys
from pathlib import Path

from openai import OpenAI


CONTACT_TOOL = {
    "type": "function",
    "name": "find_contact",
    "description": "按姓名查询联系人信息,返回邮箱和手机号。",
    "parameters": {
        "type": "object",
        "properties": {
            "name": {
                "type": "string",
                "description": "联系人姓名,例如张三",
            }
        },
        "required": ["name"],
        "additionalProperties": False,
    },
    "strict": True,
}


def find_contact(name):
    contacts = json.loads(Path("contacts.json").read_text(encoding="utf-8"))
    for contact in contacts:
        if contact["name"] == name:
            return {"ok": True, "contact": contact}
    return {"ok": False, "error": f"联系人不存在: {name}"}

再加上第一次模型请求:

python
def main():
    api_key = os.getenv("OPENAI_API_KEY")
    model = os.getenv("OPENAI_MODEL")

    if not api_key:
        print("缺少环境变量: OPENAI_API_KEY", file=sys.stderr)
        return 2
    if not model:
        print("缺少环境变量: OPENAI_MODEL", file=sys.stderr)
        return 2

    client = OpenAI(api_key=api_key, timeout=60.0)
    response = client.responses.create(
        model=model,
        tools=[CONTACT_TOOL],
        instructions="需要联系人信息时,调用 find_contact。不要编造邮箱和手机号。",
        input="张三的邮箱是多少?",
        max_output_tokens=300,
    )

    print(response.output)
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

运行:

bash
uv run python scripts/contact_agent.py

这时模型不一定直接回答,它可能返回一个 function_call。输出里能看到类似这样的对象:

text
ResponseFunctionToolCall(
  arguments='{"name":"张三"}',
  call_id='call_...',
  name='find_contact',
  type='function_call'
)

这一步只是模型说“我要调用这个函数”。函数还没执行。

五、执行函数

从响应里找 function_call

python
function_calls = [
    item for item in response.output
    if item.type == "function_call"
]

如果没有函数调用,说明模型直接回答了:

python
if not function_calls:
    print(response.output_text)
    return 0

有函数调用时,取出函数名和参数:

python
tool_call = function_calls[0]
arguments = json.loads(tool_call.arguments)

if tool_call.name != "find_contact":
    print(f"未知工具: {tool_call.name}", file=sys.stderr)
    return 2

tool_result = find_contact(name=arguments["name"])
print(tool_result)

这里的 tool_call.arguments 是 JSON 字符串,要先 json.loads() 变成字典。tool_call.name 也要检查,不能模型说调什么就直接用 globals()[name] 去执行。工具映射要白名单化。

工具结果可能是成功:

json
{
  "ok": true,
  "contact": {
    "name": "张三",
    "email": "zhangsan@example.com",
    "phone": "13800000001"
  }
}

也可能是失败:

json
{
  "ok": false,
  "error": "联系人不存在: 王五"
}

失败也要返回给模型。这样模型能告诉用户“没找到联系人”,而不是乱编一个邮箱。

六、回填结果

Python 执行完函数后,要把结果交回模型。这里用 previous_response_id 关联上一次响应,再用 function_call_output 带上工具结果。

main() 后半段改成这样:

python
    function_calls = [
        item for item in response.output
        if item.type == "function_call"
    ]

    if not function_calls:
        print(response.output_text)
        return 0

    tool_call = function_calls[0]
    arguments = json.loads(tool_call.arguments)

    if tool_call.name != "find_contact":
        print(f"未知工具: {tool_call.name}", file=sys.stderr)
        return 2

    tool_result = find_contact(name=arguments["name"])

    final_response = client.responses.create(
        model=model,
        previous_response_id=response.id,
        input=[
            {
                "type": "function_call_output",
                "call_id": tool_call.call_id,
                "output": json.dumps(tool_result, ensure_ascii=False),
            }
        ],
        max_output_tokens=300,
    )

    print(final_response.output_text)
    return 0

call_id 要原样带回去。模型第一次请求工具时给了一个 call_id,Python 回填结果时用它告诉模型:这是刚才那次函数调用的结果。

输出可能是:

text
张三的邮箱是 zhangsan@example.com。

这时才算完整走完一次函数调用:模型判断要查联系人,Python 查本地文件,模型拿到结果再组织回答。

七、完整脚本

完整的 scripts/contact_agent.py

python
#!/usr/bin/env python3
"""用函数调用查询联系人。"""

import json
import os
import sys
from pathlib import Path

from openai import OpenAI


CONTACT_TOOL = {
    "type": "function",
    "name": "find_contact",
    "description": "按姓名查询联系人信息,返回邮箱和手机号。",
    "parameters": {
        "type": "object",
        "properties": {
            "name": {
                "type": "string",
                "description": "联系人姓名,例如张三",
            }
        },
        "required": ["name"],
        "additionalProperties": False,
    },
    "strict": True,
}


def find_contact(name):
    contacts = json.loads(Path("contacts.json").read_text(encoding="utf-8"))
    for contact in contacts:
        if contact["name"] == name:
            return {"ok": True, "contact": contact}
    return {"ok": False, "error": f"联系人不存在: {name}"}


def main():
    api_key = os.getenv("OPENAI_API_KEY")
    model = os.getenv("OPENAI_MODEL")

    if not api_key:
        print("缺少环境变量: OPENAI_API_KEY", file=sys.stderr)
        return 2
    if not model:
        print("缺少环境变量: OPENAI_MODEL", file=sys.stderr)
        return 2

    client = OpenAI(api_key=api_key, timeout=60.0)
    response = client.responses.create(
        model=model,
        tools=[CONTACT_TOOL],
        instructions="需要联系人信息时,调用 find_contact。不要编造邮箱和手机号。",
        input="张三的邮箱是多少?",
        max_output_tokens=300,
    )

    function_calls = [
        item for item in response.output
        if item.type == "function_call"
    ]

    if not function_calls:
        print(response.output_text)
        return 0

    tool_call = function_calls[0]
    arguments = json.loads(tool_call.arguments)

    if tool_call.name != "find_contact":
        print(f"未知工具: {tool_call.name}", file=sys.stderr)
        return 2

    tool_result = find_contact(name=arguments["name"])

    final_response = client.responses.create(
        model=model,
        previous_response_id=response.id,
        input=[
            {
                "type": "function_call_output",
                "call_id": tool_call.call_id,
                "output": json.dumps(tool_result, ensure_ascii=False),
            }
        ],
        max_output_tokens=300,
    )

    print(final_response.output_text)
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

运行:

bash
uv run python scripts/contact_agent.py

如果把输入改成:

python
input="王五的邮箱是多少?"

工具函数会返回找不到:

json
{"ok": false, "error": "联系人不存在: 王五"}

回答会围绕“联系人不存在”,而不是补一个看起来像真的邮箱。

八、参数校验

strict=True 和 JSON Schema 能挡住一部分参数问题,但 Python 里仍然要做自己的检查。

比如 arguments 里没有 name

python
if "name" not in arguments:
    print("工具参数缺少 name", file=sys.stderr)
    return 2

或者 name 不是字符串:

python
name = arguments.get("name")
if not isinstance(name, str) or not name.strip():
    print(f"工具参数 name 不合法: {name!r}", file=sys.stderr)
    return 2

模型输出再规整,也不能替代 Python 这边的输入检查。函数调用只是在“模型想做什么”和“Python 可以做什么”之间搭了一座桥,桥的另一头还是代码。

九、只读工具

刚接函数调用时,工具放在只读范围里会稳很多:查联系人、查 FAQ、读固定文件、查本地字典。只读工具出错了,最多是回答不对;写文件、删数据、发消息这类动作,风险完全不一样。

比如这样的函数就要谨慎:

python
def delete_contact(name):
    ...

模型判断错了名字,或者用户输入里带了误导内容,就可能删掉不该删的数据。写操作要在 Python 层加确认、权限、日志和回滚,不能只靠一句提示词管住。

函数调用看着像 AI 会自己干活了,实际更像是 Python 把一小部分可控动作开放给模型。开放什么函数、参数怎么限制、结果怎么回填,这些都在代码这边。