小菜鸟

java菜鸟号正在起航

让 AI 学会“三思而后行”:用 Plan-and-Execute 架构构建复杂任务智能体

一、为什么需要 Plan-and-Execute?

传统的 ReAct 模式(Reasoning + Acting)虽然强大,但面对复杂任务时存在一个明显的缺陷:模型往往是“边想边做”,走一步看一步。这就像一个人在没有地图的情况下蒙眼走路——遇到岔路容易迷失方向,尤其是当任务包含多个依赖步骤时。

举个例子,用户提出这样的需求:

“请帮我获取关于 AI 的英文文章摘要,并把它翻译成中文。”

这个任务天然包含两个有依赖关系的步骤:

  1. 必须先获取英文摘要(get_article_summary
  2. 然后再翻译成中文(translate_text

如果让普通 Agent 来处理,它可能会:

  • 直接调用翻译工具(但还没有拿到原文)
  • 或者先获取摘要后忘记翻译
  • 或者在多步之间陷入混乱

Plan-and-Execute 架构正是为了解决这个问题而生的。它的核心理念是:

先规划(Plan),后执行(Execute) —— 就像项目经理先制定蓝图,再分配给团队成员执行。

二、架构原理:把 Agent 拆成“大脑”和“双手”

Plan-and-Execute 架构将传统的 Agent 一分为二:

组件 英文名 职责 类比
规划器 Planner 分析复杂任务,拆解成有序的步骤列表 项目经理
执行器 Executor 拿着工具列表,逐步执行计划 一线工程师

工作流程:

用户输入 → Planner(生成步骤列表)→ Executor(逐步执行)→ 返回结果

这种分离带来的好处:

  • 逻辑清晰:规划阶段不执行,执行阶段不思考
  • 可追溯:可以看到每一步的执行计划
  • 容错性强:某一步失败可以重新规划

三、完整代码解析

3.1 环境准备

阅读全文 »

从零开始:用 LangChain 构建带记忆的 AI 智能体(Agent)

前言

OpenAI 的函数调用(Function Calling)功能让大模型可以调用外部工具,而 LangChain 则把这一过程封装得更加优雅。今天,我将带你一步步构建一个带记忆功能的 AI Agent,让它不仅能调用工具查询天气,还能记住你和它的每一次对话。

最终效果如下:

🤖 Agent 已就绪,输入 '退出' 结束对话...

👤 你: 北京天气怎么样?
🤖 Agent: 北京当前天气晴朗,气温25°C,微风。

👤 你: 那上海呢?
🤖 Agent: 上海现在下着小雨,气温20°C,东南风。

👤 你: 我刚才问的第一个城市是哪里?
🤖 Agent: 您第一个问的是北京的天气情况。

整体架构

我们的 Agent 由三部分组成:

组件 作用 代码对应
大脑 理解用户意图,决定是否调用工具 ChatOpenAI
手脚 执行具体任务(如查天气) @tool 装饰的函数
记忆 记住上下文和历史对话 InMemorySaver

环境准备

pip install langchain langchain-openai langgraph

如果你使用的是阿里云百炼平台(本代码示例使用 qwen3-max),还需要配置相应的 API Key。

阅读全文 »

Transformer架构全景:从底层原理到设计精妙

如果说深度学习是人工智能的引擎,那么Transformer无疑是这台引擎中最精密的“心脏”。它不仅彻底取代了RNN和LSTM,更成为了所有现代大语言模型(如GPT、BERT、Llama)的基石。

要真正理解Transformer,我们不能只停留在“它是什么”,更要探究“它为什么这样设计”。今天,我们将从底层原理出发,进行一次彻底的架构拆解。

告别“金鱼记忆”:RNN的根本性缺陷

在Transformer出现之前,处理序列数据(如句子)的主流是循环神经网络(RNN)。你可以把它想象成一个正在排队传话的游戏:信息必须按顺序,从第一个词传递到最后一个词。

这种方式有两个致命的根本性缺陷:

  1. 无法并行计算:处理第10个词时,必须等前9个词都处理完毕。这导致训练速度极慢,GPU强大的并行计算能力完全无法施展。
  2. 长距离依赖路径过长:这是RNN的“阿喀琉斯之踵”。信息从序列开头传递到结尾,需要经过N个时间步。路径越长,梯度在反向传播时就越容易消失或爆炸,导致模型“忘记”开头的信息。例如,在“我出生在法国…我会说法语”这句话中,RNN很难将开头的“法国”和结尾的“法语”有效关联起来。

Transformer的出现,正是为了从根本上解决这两个问题。

transformer架构

Transformer 模型的整体结构

Transformer 由 编码器(Encoder)解码器(Decoder) 两部分组成:

  • 编码器:由多个相同的层堆叠而成,每层包含:

    1. 自注意力机制(Self-Attention)
    2. 前馈神经网络(Feed-Forward Network)
    3. 残差连接(Residual Connection)
    4. 层归一化(Layer Normalization)
  • 解码器:与编码器结构类似,但多了一个编码器-解码器注意力机制(Encoder-Decoder Attention)


Transformer 各个组件的功能和联系

1. 嵌入表示层(Embedding Layer)
  • 功能:将输入的离散词 ID 转换为连续向量表示(词向量)。
  • 输入:词的索引(如 [102, 567, …])
  • 输出:词向量(如 [d_model, d_model, …])
  • 注意:词向量维度通常等于 d_model,即模型的隐藏层维度。

与位置编码结合:嵌入表示层输出的向量通常会与 位置编码(Positional Encoding) 相加,以引入位置信息。


位置编码 (Positional Encoding) 的组件。这是一种包含位置信息的向量,通常使用不同频率的正弦和余弦函数生成

它将“我在第几个位置”的信息,以一种固定的模式注入到每个词的向量中。这样,模型既能并行计算,又能清晰地分辨出词语的先后顺序。

在不使用位置编码时,每个词出现在句子中的任何位置计算出来的注意力分数都是一样的,也就是的 你欠我100块钱 == 我欠你100块钱

嵌入层只在第一层存在,保证编码输入在所有层中都是稳定的

2. 注意力层(Attention Layer)
  • 功能:计算输入之间的相关性,决定哪些词在处理当前词时更重要。用来整合上下文语义,是的序列中任意两个单词之间的依赖关系可以直接被建模而不基于传统的循环结构,从而更好地解决文本的长程依赖。它的核心思想是:让句子中的每个词都能直接“看到”并“关注”到句子中的其他所有词,无论距离多远。

    这个过程可以用一个精妙的数学公式来描述:缩放点积注意力 (Scaled Dot-Product Attention)

    这个公式看起来很复杂,但我们可以将其拆解为四个直观的步骤:

    1. 生成Q, K, V:对于输入序列中的每个词,模型都会通过线性变换生成三个向量:

      • 查询 (Query, Q):代表当前词“想寻找”什么信息。
      • 键 (Key, K):代表每个词“拥有”什么信息,用于被查询。
      • 值 (Value, V):代表每个词包含的“实际内容”。
    2. 计算相关性 (QKᵀ):通过计算查询向量Q和所有键向量K的点积,我们得到了一个分数矩阵。这个分数代表了“当前词”与“句子中所有词”的相关性。分数越高,说明两者关系越紧密。

    3. 缩放与归一化 (除以√dₖ并Softmax)

      • 缩放 (Scaling):为什么要除以√dₖ?当向量维度dₖ很高时,点积的结果会变得非常大,导致Softmax函数进入梯度极小的区域(饱和区),从而阻碍模型学习。除以√dₖ可以将数值拉回到一个合适的范围,保证梯度的稳定流动。
      • 归一化 (Softmax):将上一步得到的分数通过Softmax函数转换成一个概率分布。这些概率就是“注意力权重”,决定了在聚合信息时,应该给每个词的值向量V分配多少“注意力”。前向传播阶段(防止梯度爆炸)
阅读全文 »

Pandas 数据分析:80% 的时间在清洗,这篇帮你省掉一半

做数据分析的人都知道一句话:“垃圾进,垃圾出”。不管你的图表画得多漂亮、模型多复杂,数据本身有问题,结论就是错的。

数据科学家 80% 的时间花在数据清洗和准备上。不是算法难,是数据脏——缺失值、重复行、格式混乱、异常值、字段名不一致……

Pandas 就是干这个的。它是 Python 数据分析的基石,能帮你把脏数据变成干净、可分析的表格。

Pandas 的两个核心结构:Series 和 DataFrame

结构 维度 类似
Series 1维,一列数据 Excel 里的一列
DataFrame 2维,表格 Excel 里的整张表
import pandas as pd
import numpy as np

# DataFrame 就像一张表
df = pd.DataFrame({
    '姓名': ['张三', '李四', '王五'],
    '年龄': [25, 30, 35],
    '城市': ['北京', '上海', '广州']
})

日常 99% 的工作都在跟 DataFrame 打交道——加载、清洗、筛选、聚合、输出。

拿到数据第一件事:先”体检”

假设你拿到一份销售订单数据 sales.csv,不要急着分析,先体检。

看一眼数据长什么样:

df = pd.read_csv('sales.csv')

df.head()      # 前5行,看列名和样例数据
df.tail()      # 后5行,看数据末尾有没有异常
df.sample(5)   # 随机5行,避免排序带来的偏见

看整体概况:多少行、多少列、每列是什么类型、有没有空值

df.info()

输出里重点关注:

  • Non-Null Count:小于总行数说明有缺失值
  • Dtype:object 表示字符串,需要确认是否需要转成日期或数字

看数值列的统计摘要:

阅读全文 »

SQLAlchemy 2.0:Python 最强大的 ORM,这次终于不”重”了

写过 Python Web 的人应该都听过 SQLAlchemy——Python 生态里最成熟、功能最全的 ORM。但很多人被它的学习曲线劝退了,觉得”太重”。

2.0 版本不太一样了:查询语法统一了、异步原生支持了、类型提示完整了。它不再是那个”功能强大但难上手”的框架,而是一个现代 Python 的标配工具。

SQLAlchemy 是什么?解决什么问题?

写 Python 操作数据库,最原始的方式是拼 SQL 字符串:

cursor.execute(f"SELECT * FROM users WHERE age > {age}")

拼字符串容易出事——SQL 注入、类型错误、不同数据库语法不一样。

ORM(对象关系映射)把”数据库表”映射成”Python 类”,你操作对象就是在操作数据库。SQLAlchemy 是 Python 里最成熟的 ORM。

SQLAlchemy 2.0 的三个核心变化:

  1. 查询统一了:1.x 里 Core 和 ORM 两套 API,2.0 统一用 select()
  2. 异步原生支持async/await 操作数据库,配合 FastAPI 很顺畅
  3. 类型提示完善Mapped + mapped_column,IDE 能帮你自动补全

准备工作:安装和连接

pip install sqlalchemy
pip install asyncpg  # PostgreSQL 异步驱动
pip install aiosqlite # SQLite 异步驱动(开发用)

同步连接(简单场景):

from sqlalchemy import create_engine

engine = create_engine("sqlite:///./app.db", echo=True)

异步连接(生产推荐):

from sqlalchemy.ext.asyncio import create_async_engine

engine = create_async_engine(
    "postgresql+asyncpg://user:password@localhost:5432/mydb",
    echo=False,
    pool_size=10,
    max_overflow=20,
)

echo=True 会在控制台打印 SQL 语句,开发阶段方便调试,生产环境关掉。

定义模型:一张表就是一个类

from datetime import datetime
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy import String, Boolean, func

class Base(DeclarativeBase):
    pass

class User(Base):
    __tablename__ = "users"
    
    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
    username: Mapped[str] = mapped_column(String(50), unique=True, nullable=False)
    email: Mapped[str | None] = mapped_column(String(100), nullable=True)
    is_active: Mapped[bool] = mapped_column(Boolean, default=True)
    created_at: Mapped[datetime] = mapped_column(server_default=func.now())

关键点:

  • __tablename__ 指定数据库表名
  • Mapped[T] 声明字段类型,IDE 能识别
  • mapped_column() 定义列属性(类型、长度、唯一性、默认值)
  • server_default=func.now() 表示默认值由数据库生成,不是 Python 生成

创建表:

async def init_db():
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)

CRUD:增删改查

准备工作:创建会话工厂

from sqlalchemy.ext.asyncio import async_sessionmaker

AsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False)

async def get_db():
    async with AsyncSessionLocal() as session:
        yield session

创建(Create):

async def create_user(db, username: str, email: str = None):
    user = User(username=username, email=email)
    db.add(user)
    await db.commit()
    await db.refresh(user)  # 刷新获取自增 id
    return user

refresh 让对象从数据库重新加载一遍,拿到 id 这种自动生成的值。

批量创建:

async def create_users(db, users_data: list):
    users = [User(**data) for data in users_data]
    db.add_all(users)
    await db.commit()
    return users

查询(Read):

from sqlalchemy import select

async def get_user_by_id(db, user_id: int):
    stmt = select(User).where(User.id == user_id)
    result = await db.execute(stmt)
    return result.scalar_one_or_none()

async def get_active_users(db, limit: int = 100):
    stmt = select(User).where(User.is_active == True).limit(limit)
    result = await db.execute(stmt)
    return result.scalars().all()

更新(Update):

async def update_user_email(db, user_id: int, new_email: str):
    user = await get_user_by_id(db, user_id)
    if user:
        user.email = new_email
        await db.commit()
        await db.refresh(user)
    return user

删除(Delete):

async def delete_user(db, user_id: int) -> bool:
    user = await get_user_by_id(db, user_id)
    if user:
        await db.delete(user)
        await db.commit()
        return True
    return False

关系映射:表之间怎么关联

一对多:一个用户有多篇文章

class User(Base):
    __tablename__ = "users"
    # ...
    posts: Mapped[list["Post"]] = relationship(
        back_populates="author",
        lazy="selectin"   # 预加载,避免 N+1
    )

class Post(Base):
    __tablename__ = "posts"
    
    id: Mapped[int] = mapped_column(primary_key=True)
    title: Mapped[str] = mapped_column(String(200))
    user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
    
    author: Mapped["User"] = relationship(back_populates="posts")

lazy="selectin" 的意思是:查询用户的时候,顺便把关联的文章也查出来。不加的话,访问 user.posts 时会再触发一次查询(这就是 N+1 问题)。

多对多:用户和角色

from sqlalchemy import Table, Column, Integer, ForeignKey

user_role_table = Table(
    "user_roles",
    Base.metadata,
    Column("user_id", Integer, ForeignKey("users.id")),
    Column("role_id", Integer, ForeignKey("roles.id")),
)

class Role(Base):
    __tablename__ = "roles"
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str] = mapped_column(String(50), unique=True)
    users: Mapped[list["User"]] = relationship(
        secondary=user_role_table,
        back_populates="roles"
    )

# User 里加:
roles: Mapped[list["Role"]] = relationship(
    secondary=user_role_table,
    back_populates="users"
)

secondary 指定中间表,多对多就是通过中间表关联两个表。

预加载关联数据:

from sqlalchemy.orm import selectinload, joinedload

# 查用户的时候把文章也带出来
stmt = select(User).options(selectinload(User.posts))
users = await db.execute(stmt)

# 查文章的时候把作者也带出来
stmt = select(Post).options(joinedload(Post.author))
posts = await db.execute(stmt)

selectinload 是额外查一次(两条 SQL),joinedload 是 JOIN 一次性查(一条 SQL)。一对多场景用 selectinload 更安全,不会出现数据膨胀。

过滤、排序、分页

from sqlalchemy import select, and_, or_, desc

# 条件过滤
stmt = select(User).where(User.age > 18)
stmt = select(User).where(and_(User.age > 18, User.is_active == True))
stmt = select(User).where(or_(User.role == "admin", User.role == "superuser"))

# IN 查询
stmt = select(User).where(User.id.in_([1, 2, 3, 4]))

# 模糊匹配
stmt = select(User).where(User.username.like("%admin%"))

# 排序
stmt = select(User).order_by(desc(User.created_at))

# 分页
page = 2
per_page = 20
stmt = select(User).offset((page-1)*per_page).limit(per_page)

聚合查询:

from sqlalchemy import func

# 计数
total = await db.execute(select(func.count()).select_from(User))

# 按状态分组统计
stmt = select(User.is_active, func.count(User.id)).group_by(User.is_active)
results = await db.execute(stmt)

异步:2.0 最重要的特性

SQLAlchemy 2.0 的异步是原生的,不是靠线程池模拟的。

# 所有数据库操作都加 await
async def get_user_stats(db):
    stmt = select(User.is_active, func.count(User.id)).group_by(User.is_active)
    result = await db.execute(stmt)
    return result.all()

# 批量操作
async def batch_update(db, user_ids: list, is_active: bool):
    stmt = select(User).where(User.id.in_(user_ids))
    result = await db.execute(stmt)
    users = result.scalars().all()
    
    for user in users:
        user.is_active = is_active
    
    await db.commit()

FastAPI 集成:

from fastapi import FastAPI, Depends
from sqlalchemy.ext.asyncio import AsyncSession

app = FastAPI()

@app.get("/users/{user_id}")
async def get_user(user_id: int, db: AsyncSession = Depends(get_db)):
    user = await get_user_by_id(db, user_id)
    if not user:
        return {"error": "not found"}
    return user

@app.post("/users")
async def create_user(username: str, db: AsyncSession = Depends(get_db)):
    return await create_user(db, username)

生产环境配置

连接池配置:

engine = create_async_engine(
    "postgresql+asyncpg://user:pass@localhost/db",
    pool_size=10,        # 连接池大小
    max_overflow=20,     # 超出 pool_size 最多额外创建多少
    pool_timeout=30,     # 获取连接超时(秒)
    pool_recycle=3600,   # 连接回收时间(秒)
    pool_pre_ping=True,  # 使用前检查连接是否还活着
)

事务管理:

async def transfer_money(db, from_id, to_id, amount):
    try:
        # 加悲观锁,防止并发问题
        from_user = await db.execute(
            select(User).where(User.id == from_id).with_for_update()
        )
        # ...
        await db.commit()
    except Exception:
        await db.rollback()
        raise

数据库迁移(Alembic):

pip install alembic
alembic init alembic
alembic revision --autogenerate -m "add user table"
alembic upgrade head

性能优化:N+1 和批量操作

N+1 查询:最常见的问题

# 问题代码:先查用户列表(1次查询),每个用户访问 posts 时再查一次(N次)
users = await db.execute(select(User))
for user in users.scalars():
    print(len(user.posts))   # 触发 N 次额外查询

# 解决方案:用 selectinload 预加载
stmt = select(User).options(selectinload(User.posts))
users = await db.execute(stmt)

批量插入:比逐条插入快几十倍

from sqlalchemy import insert

#  逐条插入,慢
for i in range(10000):
    db.add(User(username=f"user_{i}"))
await db.commit()

#  批量插入,快
stmt = insert(User).values([{"username": f"user_{i}"} for i in range(10000)])
await db.execute(stmt)
await db.commit()

只查需要的字段,别 SELECT *

# 只需要 id 和 username,别查所有字段
stmt = select(User.id, User.username).where(User.is_active == True)
0%