Skip to content

1.1 函数基础:打造可复用的 Agent 组件

引言:代码重用的艺术

想象你需要在 Agent 的 10 个不同地方调用 OpenAI API。你会复制粘贴 10 次相同的代码吗?绝对不会!这就是函数存在的意义:DRY (Don't Repeat Yourself) 原则。

小白理解:函数就像是一个魔法盒子

  • 你把东西放进去(输入/参数)
  • 盒子里发生一些操作(函数体)
  • 然后出来一个结果(返回值)

最重要的是:这个魔法盒子可以反复使用!写一次,用无数次。

函数是编程的基本构建块。在 AI Agent 开发中,你会用函数来:

  • 封装 API 调用
  • 处理和转换数据
  • 实现业务逻辑
  • 构建可测试的组件

本节将教你如何编写专业级的 Python 函数,为后续的 Agent 开发打下坚实基础。

学习目标

  • ✅ 掌握函数的定义与调用
  • ✅ 理解参数传递的各种形式
  • ✅ 使用类型注解编写清晰的函数签名
  • ✅ 编写标准的函数文档字符串
  • ✅ 实战:封装 LLM API 调用

第一部分:函数的定义与调用

为什么需要函数?

小白解读:想象你是一个餐厅的厨师,每次有客人点"番茄炒蛋",你都要从头想怎么做:

  • 先打蛋、再切番茄、加盐、热油...

这太累了!聪明的做法是:写一份菜谱(函数),以后每次做这道菜,照着菜谱来就行。

函数就是程序员的"菜谱"!

最简单的函数

python
def greet() -> None:
    """打印问候语"""
    print("Hello, AI Agent!")

# 调用函数
greet()  # 输出: Hello, AI Agent!

解析

  • def: 定义函数的关键字(define 的缩写)
  • greet: 函数名(使用 snake_case,即小写+下划线)
  • (): 参数列表(这里为空,表示不需要输入)
  • -> None: 返回类型注解(None 表示不返回值)
  • """...""": 文档字符串(docstring),解释函数是干什么的

图解函数结构

python
def greet() -> None:
│   │      │    │
│   │      │    └── 返回类型:告诉别人这个函数会返回什么
│   │      └── 参数列表:函数需要什么输入
│   └── 函数名:给函数起的名字
└── 关键字:告诉 Python "我要定义一个函数"

带参数的函数

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" 填入空格。

带返回值的函数

python
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 语句的函数会返回值
  • 没有 returnreturn None 的函数返回 None
  • return 后的代码不会执行(函数立即退出)

第二部分:参数传递机制

为什么有这么多种参数?

小白解读:想象你去餐厅点餐:

  • 必点项(位置参数):"我要一份牛排"
  • 可选项(默认参数):"五分熟"(不说就默认七分熟)
  • 指名道姓(关键字参数):"配菜要西兰花,饮料要可乐"
  • 随便加(*args, **kwargs):"还有...还有...再来个甜点"

Python 的参数设计也是这样,既灵活又有规则!

位置参数(Positional Arguments)

python
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)

python
# 使用关键字参数:顺序无关紧要
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)

python
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
)

小白理解 - 默认参数

默认参数就像"预设选项":

python
model: 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)

python
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")

所以 * 的含义是:"把后面所有的位置参数都收集起来"

python
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}

所以 ** 的含义是:"把后面所有的关键字参数都收集起来"


第三部分:返回值处理

单返回值

python
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

多返回值(使用元组)

python
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 允许函数返回多个值(其实是返回一个元组):

python
return char_count, word_count, avg_word_len
       └─────────────┬─────────────┘

       实际返回 (19, 3, 6.33) 这个元组

然后你可以"解包"这个元组:

python
chars, words, avg_len = analyze_text("AI Agent Development")
  │      │       │
  ↓      ↓       ↓
  19     3      6.33

就像拆快递一样,一个包裹拆出三样东西!

返回字典(更清晰的多返回值)

python
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)

python
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}")

小白理解 - 早返回模式

传统写法(嵌套深):

python
def process(text):
    if text:
        if len(text) <= 1000:
            if text.isalpha():
                # 主逻辑在这里
                return text.lower()

早返回写法(扁平化):

python
def process(text):
    if not text:
        return None
    if len(text) > 1000:
        return None
    if not text.isalpha():
        return None
    # 主逻辑在这里
    return text.lower()

早返回的好处:代码更扁平、更易读、更少嵌套。


第四部分:类型注解与文档字符串

为什么需要类型注解?

小白解读:类型注解就像给函数写"说明书":

没有类型注解:

python
def process(data, count):
    ...

看到这个,你会问:data 是什么?字符串?列表?count 是整数还是浮点数?

有类型注解:

python
def process(data: list[str], count: int) -> dict:
    ...

一目了然!data 是字符串列表,count 是整数,返回字典。

完整的类型注解示例

python
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

类型注解最佳实践

python
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 调用函数:

python
"""
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()

本节总结

核心要点

  1. 函数是代码复用的基础 - DRY 原则
  2. 类型注解是必须的 - 让代码自文档化
  3. 文档字符串很重要 - 未来的你会感谢现在的你
  4. 参数顺序: 位置参数 → 默认参数 → *args → **kwargs
  5. 早返回模式 - 减少嵌套,提高可读性

核心概念一览表

概念一句话解释生活比喻
函数可复用的代码块菜谱
参数函数的输入做菜需要的食材
返回值函数的输出做好的菜
位置参数按顺序传入排队买票
关键字参数指名道姓传入点名叫号
默认参数有预设值的参数默认七分熟
类型注解说明参数/返回值类型产品说明书

最佳实践清单

  • ✅ 函数名使用小写+下划线(snake_case)
  • ✅ 函数应该只做一件事(单一职责)
  • ✅ 使用类型注解
  • ✅ 编写文档字符串
  • ✅ 默认参数使用不可变对象
  • ✅ 参数验证放在函数开头
  • ✅ 使用早返回处理错误情况

下一节:1.2 高阶函数与函数式编程

基于 MIT 许可证发布。内容版权归作者所有。