欢迎光临
我们一直在努力

《LLM Agent 从入门到生产部署:Spec驱动开发与上下文工程深度实战》第1章 Agent本质论:从LLM调用到自主智能体

欢迎来到本系列的第一章。很多人认为“Agent”不过是在LLM外面套一层循环调用工具。但真正理解Agent的本质——如何感知环境、规划行动、执行反馈并循环迭代——才是从“调用API”走向“构建自主系统”的关键。本章将从零开始,定义一个可运行的ReAct Agent,并引出后续所有章节将要解决的痛点。

1.1 为什么需要Agent?从单一问答到任务闭环

传统的大模型调用是“无状态、单回合”的:用户输入 → LLM → 输出。这种模式适用于翻译、摘要、简单的知识问答。但当你需要模型执行一个多步骤的任务,比如“帮我查一下北京未来三天的天气,如果下雨就提醒我带伞,否则推荐一个公园”,模型就必须:

  • 调用天气API获取数据

  • 根据结果做出判断

  • 再次生成最终回答

  • 这就是Agent的核心价值:让LLM成为“大脑”,能够自主调用外部工具、规划步骤、处理异常,最终完成一个闭环任务。

    1.1.1 Agent 的“感知-规划-行动”循环

    一个典型的Agent循环包含三个步骤:

    while not done:
    1. 感知(Perceive):将当前环境状态(用户输入、历史消息、工具返回结果)组装成上下文
    2. 规划(Plan):LLM决定下一步要做什么(是调用某个工具,还是直接输出最终答案)
    3. 行动(Act):执行LLM选择的行为(调用工具函数,或者返回结果给用户)

    下面这张图展示了最简单的ReAct Agent循环流程(使用Mermaid语法):

    1.2 经典Agent架构:ReAct、Plan-and-Execute、Reflexion

    1.2.1 ReAct(Reason + Act)

    ReAct是最经典的范式:LLM在每一步交替输出“思考”(Reason)和“行动”(Act)。例如:

    Thought: 我需要知道北京今天的天气才能回答用户。
    Action: get_weather(city="北京")
    Observation: 晴天,25°C
    Thought: 天气晴朗,不需要提醒带伞,可以推荐公园。
    Action: recommend_park(city="北京")
    Observation: 朝阳公园、奥林匹克森林公园
    Thought: 我现在可以给出最终答案了。
    Answer: 北京今天晴天,25°C,推荐去朝阳公园或奥林匹克森林公园。

    优点:简单直观,易于调试。
    缺点:对于复杂任务,思维链可能过长,容易陷入循环。

    1.2.2 Plan-and-Execute(先规划后执行)

    该模式先让LLM生成一个完整计划(步骤列表),然后逐步执行每个步骤,必要时动态调整。

    Plan:
    1. 调用天气API获取北京未来三天天气
    2. 如果包含雨天,则生成提醒
    3. 搜索北京公园推荐
    4. 合并结果输出

    优点:对长时间任务更稳定,减少重复推理。
    缺点:计划可能与实际结果不符,需要纠错机制。

    1.2.3 Reflexion(反思)

    Reflexion在Agent执行任务后,增加一个“反思”步骤:评估结果是否满意,如果不满意则生成改进建议,并重试。常用于编程、数学等有明确反馈的任务。

    本系列将主要基于ReAct展开,因为它是理解Agent最小闭环的最佳起点,后续章节会融入Plan-and-Execute和Reflexion的元素。

    1.3 工具(Tools)的概念与设计

    Agent的能力取决于它能使用的工具。工具可以是任何函数:查询数据库、调用API、运行代码、发送邮件等。关键在于如何让LLM理解工具的存在,并选择正确的工具。

    1.3.1 Function Calling 接口

    OpenAI、Anthropic、Google等主流模型都支持Function Calling(或Tool Use)。你向模型提供一组工具的描述(名称、参数Schema),模型返回一个结构化的tool_calls对象,而不是纯文本。

    一个工具描述的例子(OpenAI格式):

    {
    "type": "function",
    "function": {
    "name": "get_weather",
    "description": "获取指定城市的当前天气",
    "parameters": {
    "type": "object",
    "properties": {
    "city": {
    "type": "string",
    "description": "城市名,例如 '北京'"
    }
    },
    "required": ["city"]
    }
    }
    }

    模型调用时返回:

    {
    "tool_calls": [{
    "id": "call_abc123",
    "function": {
    "name": "get_weather",
    "arguments": "{\\"city\\":\\"北京\\"}"
    }
    }]
    }

    1.3.2 本地模型与ReAct提示词

    如果你使用本地模型(如Llama 3、Qwen),它们不一定支持Function Calling。此时可以退化为提示词引导,例如:

    你是一个助手,可以使用以下工具:
    – get_weather(city): 获取城市天气,返回字符串。
    – calculate(expression): 计算数学表达式。

    当你需要使用工具时,请输出以下格式:
    Action: 工具名称
    Action Input: 参数JSON

    当你已经获得足够信息,输出最终答案:
    Final Answer: …

    我们在本章的代码实例中将同时演示OpenAI官方Function Calling和本地模型提示词两种方式。

    1.4 实战:搭建一个最小ReAct Agent

    我们将构建一个支持天气查询和计算器的Agent。用户输入混合任务,Agent自主决定调用哪个工具,并最终给出答案。

    1.4.1 环境准备

    安装依赖:

    pip install openai python-dotenv httpx
    # 如果使用本地模型,需要安装 ollama 或 llama.cpp,此处以OpenAI为例

    创建.env文件:

    OPENAI_API_KEY=sk-…

    1.4.2 定义工具函数

    # tools.py
    import httpx
    import json

    def get_weather(city: str) -> str:
    """模拟天气API,实际可换成真实API"""
    # 这里使用 wttr.in 的简单接口
    try:
    response = httpx.get(f"https://wttr.in/{city}?format=%C+%t", timeout=5)
    if response.status_code == 200:
    return response.text.strip()
    else:
    return f"无法获取{city}天气"
    except Exception as e:
    return f"天气服务异常: {str(e)}"

    def calculate(expression: str) -> str:
    """安全地计算数学表达式"""
    # 注意:eval有安全风险,生产环境应使用受限的解析器如numexpr或ast.literal_eval
    # 本演示仅用于教学,只允许简单四则运算
    allowed_chars = set("0123456789+-*/(). ")
    if not all(c in allowed_chars for c in expression):
    return "表达式包含非法字符"
    try:
    result = eval(expression, {"__builtins__": {}}, {})
    return str(result)
    except Exception as e:
    return f"计算错误: {str(e)}"

    # 工具注册表
    TOOLS = {
    "get_weather": get_weather,
    "calculate": calculate,
    }

    # 提供给OpenAI Function Calling 的工具描述
    TOOLS_DEFINITION = [
    {
    "type": "function",
    "function": {
    "name": "get_weather",
    "description": "获取指定城市的当前天气情况",
    "parameters": {
    "type": "object",
    "properties": {
    "city": {"type": "string", "description": "城市名称,如'北京'"}
    },
    "required": ["city"]
    }
    }
    },
    {
    "type": "function",
    "function": {
    "name": "calculate",
    "description": "执行数学计算,支持加减乘除和括号",
    "parameters": {
    "type": "object",
    "properties": {
    "expression": {"type": "string", "description": "数学表达式,如'3*(4+5)'"}
    },
    "required": ["expression"]
    }
    }
    }
    ]

    1.4.3 Agent 核心循环(OpenAI Function Calling版)

    # agent_openai.py
    import os
    from dotenv import load_dotenv
    from openai import OpenAI
    import json
    from tools import TOOLS, TOOLS_DEFINITION

    load_dotenv()
    client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))

    class SimpleAgent:
    def __init__(self, model="gpt-4o-mini", max_iterations=5):
    self.model = model
    self.max_iterations = max_iterations
    self.tools = TOOLS
    self.tools_def = TOOLS_DEFINITION

    def run(self, user_input: str) -> str:
    messages = [{"role": "user", "content": user_input}]
    for step in range(self.max_iterations):
    # 调用LLM,附带工具定义
    response = client.chat.completions.create(
    model=self.model,
    messages=messages,
    tools=self.tools_def,
    tool_choice="auto", # 让模型自己决定是否调用工具
    )
    msg = response.choices[0].message
    messages.append(msg)

    # 检查是否有工具调用
    if msg.tool_calls:
    # 处理所有工具调用(可并行,这里串行演示)
    for tool_call in msg.tool_calls:
    tool_name = tool_call.function.name
    tool_args = json.loads(tool_call.function.arguments)
    if tool_name in self.tools:
    tool_result = self.tools[tool_name](**tool_args)
    # 将工具结果追加到对话中
    messages.append({
    "role": "tool",
    "tool_call_id": tool_call.id,
    "content": tool_result
    })
    else:
    messages.append({
    "role": "tool",
    "tool_call_id": tool_call.id,
    "content": f"未知工具: {tool_name}"
    })
    else:
    # 没有工具调用,说明Agent已准备好最终答案
    return msg.content
    return "达到最大迭代次数,无法完成请求"

    if __name__ == "__main__":
    agent = SimpleAgent()
    queries = [
    "北京今天天气怎么样?",
    "计算 (25 + 35) * 2 等于多少?",
    "上海现在天气如何?如果温度低于10度,告诉我多穿衣服,否则告诉我适合户外运动。"
    ]
    for q in queries:
    print(f"\\n用户: {q}")
    answer = agent.run(q)
    print(f"Agent: {answer}")

    运行结果示例:

    用户: 北京今天天气怎么样?
    Agent: 北京今天的天气是晴天,温度约为25°C。

    用户: 计算 (25 + 35) * 2 等于多少?
    Agent: (25 + 35) * 2 = 120。

    用户: 上海现在天气如何?如果温度低于10度,告诉我多穿衣服,否则告诉我适合户外运动。
    Agent: 上海当前天气为多云,温度15°C,适合户外运动。

    1.4.4 本地模型版本(使用Ollama + 提示词驱动)

    如果你没有OpenAI API,可以使用本地模型(如Llama 3.1 8B)通过Ollama运行。安装Ollama后,拉取模型:

    ollama pull llama3.1

    然后实现一个基于提示词的Agent(不依赖Function Calling):

    # agent_local.py
    import json
    import ollama
    from tools import TOOLS

    class LocalAgent:
    def __init__(self, model="llama3.1", max_iterations=5):
    self.model = model
    self.max_iterations = max_iterations
    self.tools = TOOLS

    def _build_prompt(self, messages):
    """将消息列表转换为提示词格式"""
    prompt = ""
    for m in messages:
    role = m["role"]
    content = m["content"]
    if role == "system":
    prompt += f"System: {content}\\n"
    elif role == "user":
    prompt += f"User: {content}\\n"
    elif role == "assistant":
    prompt += f"Assistant: {content}\\n"
    elif role == "tool":
    prompt += f"Observation: {content}\\n"
    prompt += "Assistant: "
    return prompt

    def run(self, user_input: str) -> str:
    system_prompt = f"""你是一个能使用工具的助手。你可以调用以下函数:
    {', '.join(self.tools.keys())}。

    当需要调用工具时,严格按以下格式输出:
    Action: 函数名
    Action Input: JSON字符串,例如 {{"city": "北京"}}

    当你准备好最终答案时,输出:
    Final Answer: 你的回答

    请一步一步思考。"""
    messages = [
    {"role": "system", "content": system_prompt},
    {"role": "user", "content": user_input}
    ]

    for step in range(self.max_iterations):
    prompt = self._build_prompt(messages)
    response = ollama.generate(model=self.model, prompt=prompt, options={"temperature": 0})
    output = response["response"].strip()
    messages.append({"role": "assistant", "content": output})

    if output.startswith("Final Answer:"):
    return output[len("Final Answer:"):].strip()
    elif output.startswith("Action:"):
    # 解析工具名和参数
    lines = output.split("\\n")
    action = None
    action_input = None
    for line in lines:
    if line.startswith("Action:"):
    action = line.split(":", 1)[1].strip()
    elif line.startswith("Action Input:"):
    raw = line.split(":", 1)[1].strip()
    try:
    action_input = json.loads(raw)
    except:
    action_input = {}
    if action and action in self.tools:
    result = self.tools[action](**action_input)
    messages.append({"role": "tool", "content": str(result)})
    else:
    messages.append({"role": "tool", "content": f"Error: 未知工具 {action}"})
    else:
    # 模型未按格式输出,尝试强制要求
    messages.append({"role": "user", "content": "请按格式输出 Action 或 Final Answer"})
    return "达到最大迭代次数"

    if __name__ == "__main__":
    agent = LocalAgent()
    print(agent.run("北京天气如何?"))

    这个版本虽然不如Function Calling稳定,但展示了ReAct提示词的本质,适合理解原理。

    1.5 当前Agent的局限与本章总结

    通过上述代码,你已经拥有一个能自动调用工具的多步Agent。但是,它存在诸多问题,正是后续章节要解决的:

    问题描述对应章节
    输出格式不稳定 本地模型可能不按JSON输出,导致解析失败 第2章(Spec驱动开发)
    无限循环 Agent可能重复调用同一个工具 第3章(状态机与终止检测)
    上下文爆炸 多轮对话后token超限 第4章(上下文窗口管理)
    知识不足 无法回答私有领域问题 第5章(RAG)
    工具安全风险 eval执行任意代码 第6章(沙箱执行)
    单点故障 无监督、无降级 第9章(可观测性与降级)
    难以部署 同步阻塞,不满足高并发 第10章(生产部署)

    本章我们奠定了Agent的基本概念和最小实现。在下一章,我们将引入Spec驱动开发——用结构化约束(JSON Schema、Pydantic)彻底解决输出格式不稳定的问题,让Agent的行为可预测、可测试。

    1.6 思考与练习

  • 尝试修改SimpleAgent,支持并行调用多个工具(提示:OpenAI支持一次返回多个tool_calls)。

  • 如果某个工具调用失败(例如天气API超时),如何让Agent自动重试或改用其他策略?

  • 分析LocalAgent中,为什么temperature=0很重要?

  • (挑战)实现一个最简单的缓存:如果用户两次问同一个城市的天气,直接返回上次结果而不调用API。

  • 答案提示:下一章将提供部分参考实现。


    下章预告:第2章「Spec驱动开发(上):用结构化约束驯服LLM」——我们将学习如何确保LLM的输出百分之百符合预期Schema,并引入instructor库自动重试与校验。

    赞(0)
    未经允许不得转载:171主机测评 » 《LLM Agent 从入门到生产部署:Spec驱动开发与上下文工程深度实战》第1章 Agent本质论:从LLM调用到自主智能体
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址