1.1 函数基础:打造可复用的 Agent 组件
引言:代码重用的艺术
想象你需要在 Agent 的 10 个不同地方调用 OpenAI API。你会复制粘贴 10 次相同的代码吗?绝对不会!这就是函数存在的意义:DRY (Don't Repeat Yourself) 原则。
小白理解:函数就像是一个魔法盒子:
- 你把东西放进去(输入/参数)
- 盒子里发生一些操作(函数体)
- 然后出来一个结果(返回值)
最重要的是:这个魔法盒子可以反复使用!写一次,用无数次。
函数是编程的基本构建块。在 AI Agent 开发中,你会用函数来:
- 封装 API 调用
- 处理和转换数据
- 实现业务逻辑
- 构建可测试的组件
本节将教你如何编写专业级的 Python 函数,为后续的 Agent 开发打下坚实基础。
学习目标
- ✅ 掌握函数的定义与调用
- ✅ 理解参数传递的各种形式
- ✅ 使用类型注解编写清晰的函数签名
- ✅ 编写标准的函数文档字符串
- ✅ 实战:封装 LLM API 调用
第一部分:函数的定义与调用
为什么需要函数?
小白解读:想象你是一个餐厅的厨师,每次有客人点"番茄炒蛋",你都要从头想怎么做:
- 先打蛋、再切番茄、加盐、热油...
这太累了!聪明的做法是:写一份菜谱(函数),以后每次做这道菜,照着菜谱来就行。
函数就是程序员的"菜谱"!
最简单的函数
def greet() -> None:
"""打印问候语"""
print("Hello, AI Agent!")
# 调用函数
greet() # 输出: Hello, AI Agent!解析:
def: 定义函数的关键字(define 的缩写)greet: 函数名(使用 snake_case,即小写+下划线)(): 参数列表(这里为空,表示不需要输入)-> None: 返回类型注解(None 表示不返回值)"""...""": 文档字符串(docstring),解释函数是干什么的
图解函数结构:
pythondef greet() -> None: │ │ │ │ │ │ │ └── 返回类型:告诉别人这个函数会返回什么 │ │ └── 参数列表:函数需要什么输入 │ └── 函数名:给函数起的名字 └── 关键字:告诉 Python "我要定义一个函数"
带参数的函数
def greet_agent(name: str) -> None:
"""
向指定的 Agent 打招呼
Args:
name: Agent 的名称
"""
print(f"Hello, {name}!")
# 调用
greet_agent("ResearchBot") # Hello, ResearchBot!
greet_agent("ChatGPT") # Hello, ChatGPT!小白理解 - 参数就是"填空":
想象这个函数是一个"填空模板":
Hello, ____! ↑ 这里需要填入名字当你调用
greet_agent("ChatGPT")时,就是把 "ChatGPT" 填入空格。
带返回值的函数
def create_greeting(name: str) -> str:
"""
创建问候语字符串
Args:
name: Agent 的名称
Returns:
格式化的问候语
"""
return f"Hello, {name}!"
# 调用并使用返回值
message: str = create_greeting("Claude")
print(message) # Hello, Claude!小白理解 - return 是什么?
return就像是把结果"交出来":输入 "Claude" ↓ ┌─────────────────────────────┐ │ create_greeting 函数 │ │ │ │ 处理:f"Hello, {name}!" │ │ │ │ return ← "把结果交出来" │ └─────────────────────────────┘ ↓ 输出 "Hello, Claude!"
- 有
return的函数:计算完会把结果给你- 没有
return的函数:只是做事,不给你结果
💡 关键概念:
- 有
return语句的函数会返回值- 没有
return或return None的函数返回Nonereturn后的代码不会执行(函数立即退出)
第二部分:参数传递机制
为什么有这么多种参数?
小白解读:想象你去餐厅点餐:
- 必点项(位置参数):"我要一份牛排"
- 可选项(默认参数):"五分熟"(不说就默认七分熟)
- 指名道姓(关键字参数):"配菜要西兰花,饮料要可乐"
- 随便加(*args, **kwargs):"还有...还有...再来个甜点"
Python 的参数设计也是这样,既灵活又有规则!
位置参数(Positional Arguments)
def calculate_cost(
input_tokens: int,
output_tokens: int,
price_per_1k: float
) -> float:
"""
计算 API 调用成本
Args:
input_tokens: 输入 token 数量
output_tokens: 输出 token 数量
price_per_1k: 每 1000 tokens 的价格
Returns:
总成本(美元)
"""
total_tokens: int = input_tokens + output_tokens
cost: float = (total_tokens / 1000) * price_per_1k
return cost
# 位置参数调用:顺序很重要!
cost = calculate_cost(1000, 500, 0.002)
print(f"${cost:.4f}") # $0.0030小白理解 - 位置参数:
位置参数就像"排队":
calculate_cost(1000, 500, 0.002) │ │ │ ↓ ↓ ↓ input_tokens output_tokens price_per_1k (第1位) (第2位) (第3位)顺序不能乱! 如果你写
calculate_cost(0.002, 500, 1000), 那 0.002 会被当成 input_tokens,完全错了!
关键字参数(Keyword Arguments)
# 使用关键字参数:顺序无关紧要
cost = calculate_cost(
output_tokens=500,
input_tokens=1000,
price_per_1k=0.002
)
print(f"${cost:.4f}") # $0.0030
# 混合使用(位置参数必须在关键字参数之前)
cost = calculate_cost(1000, output_tokens=500, price_per_1k=0.002)小白理解 - 关键字参数:
关键字参数就像"指名道姓":
"output_tokens=500" 意思是:我要把 500 给 output_tokens 这个参数好处是:
- 顺序可以随便换
- 代码更清晰,一眼就知道每个值是什么意思
- 不怕记错顺序
⚠️ 常见陷阱:
python# ❌ 错误:关键字参数在位置参数之后 cost = calculate_cost(input_tokens=1000, 500, 0.002) # SyntaxError: positional argument follows keyword argument
默认参数值(Default Arguments)
def call_llm(
prompt: str,
model: str = "gpt-3.5-turbo",
temperature: float = 0.7,
max_tokens: int = 2000
) -> str:
"""
调用 LLM API(简化版)
Args:
prompt: 提示词
model: 模型名称,默认 gpt-3.5-turbo
temperature: 温度参数,默认 0.7
max_tokens: 最大 tokens,默认 2000
Returns:
LLM 响应
"""
print(f"调用 {model},temperature={temperature},max_tokens={max_tokens}")
print(f"Prompt: {prompt}")
return f"[模拟响应] 收到提示: {prompt[:30]}..."
# 只传必需参数
response = call_llm("What is AI?")
# 覆盖部分默认值
response = call_llm("What is AI?", model="gpt-4", temperature=0.5)
# 使用所有默认值
response = call_llm(
"What is AI?",
model="claude-3-opus",
temperature=0.9,
max_tokens=4000
)小白理解 - 默认参数:
默认参数就像"预设选项":
pythonmodel: str = "gpt-3.5-turbo"意思是:"如果你不告诉我用什么模型,我就默认用 gpt-3.5-turbo"
这让函数调用变得很方便:
- 简单情况:
call_llm("问题")(用所有默认值)- 需要定制:
call_llm("问题", model="gpt-4")(只改需要的)
💡 最佳实践:
- 必需参数放在前面
- 可选参数使用默认值
- 默认值应该是不可变对象(字符串、数字、None)
⚠️ 危险的默认值:
python# ❌ 错误:使用可变对象作为默认值 def add_message(message: str, history: list = []) -> list: history.append(message) return history # 问题:所有调用共享同一个列表! h1 = add_message("Hello") # ['Hello'] h2 = add_message("World") # ['Hello', 'World'] ← 不是期望的结果! # ✅ 正确做法 def add_message(message: str, history: list | None = None) -> list: if history is None: history = [] history.append(message) return history为什么会这样? 因为 Python 中,默认值只在函数定义时创建一次。 如果默认值是可变对象(如列表),所有调用都会共用同一个对象!
可变参数(*args 和 **kwargs)
def log_messages(*messages: str) -> None:
"""
记录多条消息
Args:
*messages: 可变数量的消息字符串
"""
for i, msg in enumerate(messages, 1):
print(f"[{i}] {msg}")
# 调用
log_messages("Starting agent")
log_messages("User input", "Processing", "Generating response")*小白理解 - args 是什么?
*args就像一个"收纳袋",可以装任意数量的位置参数:log_messages("A", "B", "C") ↓ ↓ ↓ 都被装进 messages 这个元组里 messages = ("A", "B", "C")所以
*的含义是:"把后面所有的位置参数都收集起来"
def create_agent_config(**kwargs: str | int | float) -> dict:
"""
创建 Agent 配置
Args:
**kwargs: 任意关键字参数
Returns:
配置字典
"""
config: dict = {
"model": "gpt-3.5-turbo",
"temperature": 0.7,
}
config.update(kwargs)
return config
# 调用
config = create_agent_config(
model="gpt-4",
temperature=0.5,
max_tokens=4000,
custom_param="value"
)
print(config)**小白理解 - kwargs 是什么?
**kwargs就像一个"命名收纳袋",可以装任意数量的关键字参数:create_agent_config(model="gpt-4", max_tokens=4000) ↓ ↓ 都被装进 kwargs 这个字典里 kwargs = {"model": "gpt-4", "max_tokens": 4000}所以
**的含义是:"把后面所有的关键字参数都收集起来"
第三部分:返回值处理
单返回值
def get_token_count(text: str) -> int:
"""
计算文本的 token 数量(简化版)
Args:
text: 输入文本
Returns:
token 数量
"""
# 简化实现:按空格分词
return len(text.split())
count: int = get_token_count("Hello, how are you?")
print(count) # 4多返回值(使用元组)
def analyze_text(text: str) -> tuple[int, int, float]:
"""
分析文本统计信息
Args:
text: 输入文本
Returns:
(字符数, 单词数, 平均单词长度)
"""
char_count: int = len(text)
words: list[str] = text.split()
word_count: int = len(words)
avg_word_len: float = char_count / word_count if word_count > 0 else 0.0
return char_count, word_count, avg_word_len
# 解包返回值
chars, words, avg_len = analyze_text("AI Agent Development")
print(f"字符: {chars}, 单词: {words}, 平均: {avg_len:.2f}")小白理解 - 多返回值解包:
Python 允许函数返回多个值(其实是返回一个元组):
pythonreturn char_count, word_count, avg_word_len └─────────────┬─────────────┘ ↓ 实际返回 (19, 3, 6.33) 这个元组然后你可以"解包"这个元组:
pythonchars, words, avg_len = analyze_text("AI Agent Development") │ │ │ ↓ ↓ ↓ 19 3 6.33就像拆快递一样,一个包裹拆出三样东西!
返回字典(更清晰的多返回值)
def analyze_text_dict(text: str) -> dict[str, int | float]:
"""
分析文本统计信息(返回字典)
Args:
text: 输入文本
Returns:
包含统计信息的字典
"""
words: list[str] = text.split()
return {
"char_count": len(text),
"word_count": len(words),
"avg_word_length": len(text) / len(words) if words else 0.0,
}
# 使用返回值
stats = analyze_text_dict("LangChain and LangGraph")
print(stats["word_count"]) # 3
print(stats["avg_word_length"]) # 7.0小白理解 - 字典 vs 元组返回值:
返回方式 优点 缺点 元组 (a, b, c)简洁 必须记住顺序 字典 {"a": 1}清晰,可以按名字取 代码稍长 建议:返回 2-3 个值用元组,超过 3 个用字典。
早返回模式(Early Return)
def validate_and_process(
text: str,
max_length: int = 1000
) -> str | None:
"""
验证并处理文本
Args:
text: 输入文本
max_length: 最大长度
Returns:
处理后的文本,验证失败返回 None
"""
# 早返回:验证失败立即返回
if not text:
print("错误: 文本为空")
return None
if len(text) > max_length:
print(f"错误: 文本超过最大长度 {max_length}")
return None
# 主逻辑
processed = text.strip().lower()
return processed
# 使用
result = validate_and_process(" Hello World ")
if result:
print(f"处理结果: {result}")小白理解 - 早返回模式:
传统写法(嵌套深):
pythondef process(text): if text: if len(text) <= 1000: if text.isalpha(): # 主逻辑在这里 return text.lower()早返回写法(扁平化):
pythondef process(text): if not text: return None if len(text) > 1000: return None if not text.isalpha(): return None # 主逻辑在这里 return text.lower()早返回的好处:代码更扁平、更易读、更少嵌套。
第四部分:类型注解与文档字符串
为什么需要类型注解?
小白解读:类型注解就像给函数写"说明书":
没有类型注解:
pythondef process(data, count): ...看到这个,你会问:data 是什么?字符串?列表?count 是整数还是浮点数?
有类型注解:
pythondef process(data: list[str], count: int) -> dict: ...一目了然!data 是字符串列表,count 是整数,返回字典。
完整的类型注解示例
from typing import Optional, Union
def call_api(
endpoint: str,
method: str = "GET",
data: Optional[dict] = None,
timeout: float = 30.0
) -> Union[dict, None]:
"""
调用 API 端点
这是一个完整的函数文档字符串示例,遵循 Google 风格。
Args:
endpoint: API 端点 URL
method: HTTP 方法(GET, POST 等)
data: 请求数据(可选)
timeout: 超时时间(秒)
Returns:
API 响应的 JSON 数据,失败返回 None
Raises:
ValueError: 如果 method 不是有效的 HTTP 方法
ConnectionError: 如果无法连接到服务器
Examples:
>>> call_api("https://api.example.com/data")
{'status': 'success', 'data': [...]}
>>> call_api(
... "https://api.example.com/users",
... method="POST",
... data={"name": "Alice"}
... )
{'id': 123, 'name': 'Alice'}
"""
# 实现细节...
pass小白理解 - 常见类型注解:
写法 含义 str字符串 int整数 float浮点数 bool布尔值 True/False list[str]字符串列表 dict[str, int]键是字符串、值是整数的字典 str | None字符串或 None Optional[str]等同于 str | None
类型注解最佳实践
from typing import List, Dict, Tuple, Optional, Union
# ✅ 推荐:使用现代语法(Python 3.10+)
def process_messages(
messages: list[str], # 而不是 List[str]
metadata: dict[str, int], # 而不是 Dict[str, int]
result: tuple[int, str], # 而不是 Tuple[int, str]
optional_key: str | None = None # 而不是 Optional[str]
) -> str | int: # 而不是 Union[str, int]
"""处理消息列表"""
pass
# ✅ 复杂类型的别名
MessageHistory = list[dict[str, str]]
AgentConfig = dict[str, str | int | float]
def create_agent(
history: MessageHistory,
config: AgentConfig
) -> None:
"""使用类型别名使代码更清晰"""
pass第五部分:实战案例——封装 LLM API 调用
让我们构建一个真实的、可复用的 LLM API 调用函数:
"""
LLM API 调用封装
演示如何将 API 调用封装成可复用的函数
"""
from typing import Optional
import time
def call_openai_api(
prompt: str,
model: str = "gpt-3.5-turbo",
temperature: float = 0.7,
max_tokens: int = 2000,
max_retries: int = 3,
retry_delay: float = 1.0,
api_key: Optional[str] = None
) -> dict[str, str | int]:
"""
调用 OpenAI API(带重试机制)
这个函数封装了 OpenAI API 调用的复杂性,提供:
- 自动重试
- 错误处理
- Token 统计
- 标准化的响应格式
Args:
prompt: 输入提示词
model: 模型名称
temperature: 温度参数 (0-2)
max_tokens: 最大生成 tokens
max_retries: 最大重试次数
retry_delay: 重试延迟(秒)
api_key: API 密钥(可选,从环境变量读取)
Returns:
包含以下键的字典:
- 'response': LLM 响应文本
- 'model': 使用的模型
- 'tokens_used': 使用的 token 数
- 'success': 是否成功
Raises:
ValueError: 如果参数无效
RuntimeError: 如果达到最大重试次数仍失败
Examples:
>>> result = call_openai_api("What is AI?")
>>> print(result['response'])
'AI stands for Artificial Intelligence...'
>>> result = call_openai_api(
... "Explain quantum computing",
... model="gpt-4",
... temperature=0.5
... )
"""
# 参数验证
if not prompt:
raise ValueError("Prompt 不能为空")
if not (0 <= temperature <= 2):
raise ValueError("Temperature 必须在 0-2 之间")
if max_tokens <= 0:
raise ValueError("max_tokens 必须大于 0")
# 重试逻辑
for attempt in range(max_retries):
try:
print(f"[尝试 {attempt + 1}/{max_retries}] 调用 {model}...")
# 这里是实际的 API 调用
# 为了演示,我们模拟调用
response_text = f"[模拟响应] 收到提示: '{prompt[:50]}...'"
tokens_used = len(prompt.split()) + len(response_text.split())
# 模拟成功
return {
"response": response_text,
"model": model,
"tokens_used": tokens_used,
"success": True,
}
except Exception as e:
print(f"错误: {e}")
if attempt < max_retries - 1:
print(f"等待 {retry_delay} 秒后重试...")
time.sleep(retry_delay)
else:
print(f"达到最大重试次数 ({max_retries})")
return {
"response": "",
"model": model,
"tokens_used": 0,
"success": False,
}
raise RuntimeError("API 调用失败")
def format_prompt_with_context(
user_input: str,
context: list[str],
system_prompt: str = "You are a helpful AI assistant."
) -> str:
"""
格式化包含上下文的提示词
Args:
user_input: 用户输入
context: 上下文消息列表
system_prompt: 系统提示词
Returns:
格式化的完整提示词
"""
parts: list[str] = [f"System: {system_prompt}"]
if context:
parts.append("\nConversation History:")
for i, msg in enumerate(context, 1):
parts.append(f"{i}. {msg}")
parts.append(f"\nUser: {user_input}")
parts.append("Assistant:")
return "\n".join(parts)
def estimate_token_cost(
token_count: int,
model: str = "gpt-3.5-turbo"
) -> float:
"""
估算 API 调用成本
Args:
token_count: token 数量
model: 模型名称
Returns:
估算成本(美元)
"""
# 简化的定价表
pricing: dict[str, float] = {
"gpt-3.5-turbo": 0.002,
"gpt-4": 0.03,
"gpt-4-turbo": 0.01,
}
price_per_1k = pricing.get(model, 0.002)
cost = (token_count / 1000) * price_per_1k
return cost
# 演示使用
def main() -> None:
"""主函数:演示 LLM API 调用"""
# 1. 简单调用
print("=== 简单调用 ===")
result = call_openai_api("What is machine learning?")
print(f"响应: {result['response']}")
print(f"Tokens: {result['tokens_used']}")
# 2. 带上下文的调用
print("\n=== 带上下文调用 ===")
context = [
"User: Hello",
"Assistant: Hi! How can I help you?",
]
prompt = format_prompt_with_context(
"What is AI?",
context,
"You are a friendly AI tutor."
)
result = call_openai_api(prompt, model="gpt-4", temperature=0.5)
print(f"响应: {result['response']}")
# 3. 成本估算
print("\n=== 成本估算 ===")
tokens = result['tokens_used']
cost = estimate_token_cost(tokens, model="gpt-4")
print(f"使用 {tokens} tokens,估算成本: ${cost:.4f}")
if __name__ == "__main__":
main()本节总结
核心要点
- 函数是代码复用的基础 - DRY 原则
- 类型注解是必须的 - 让代码自文档化
- 文档字符串很重要 - 未来的你会感谢现在的你
- 参数顺序: 位置参数 → 默认参数 → *args → **kwargs
- 早返回模式 - 减少嵌套,提高可读性
核心概念一览表
| 概念 | 一句话解释 | 生活比喻 |
|---|---|---|
| 函数 | 可复用的代码块 | 菜谱 |
| 参数 | 函数的输入 | 做菜需要的食材 |
| 返回值 | 函数的输出 | 做好的菜 |
| 位置参数 | 按顺序传入 | 排队买票 |
| 关键字参数 | 指名道姓传入 | 点名叫号 |
| 默认参数 | 有预设值的参数 | 默认七分熟 |
| 类型注解 | 说明参数/返回值类型 | 产品说明书 |
最佳实践清单
- ✅ 函数名使用小写+下划线(snake_case)
- ✅ 函数应该只做一件事(单一职责)
- ✅ 使用类型注解
- ✅ 编写文档字符串
- ✅ 默认参数使用不可变对象
- ✅ 参数验证放在函数开头
- ✅ 使用早返回处理错误情况
下一节:1.2 高阶函数与函数式编程