Appearance
函数调用
模型自己不会读本地文件,也不会查数据库。它能做的是看完问题以后,告诉 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 0call_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 把一小部分可控动作开放给模型。开放什么函数、参数怎么限制、结果怎么回填,这些都在代码这边。