跳到正文

03. Prompt 与 Message

学习目标

完成本章后,你应该能够:

  • 说明 message 为什么是 Agent 的上下文载体。
  • 区分 System、Human、AI 和 Tool message 的职责。
  • 使用 ChatPromptTemplate 渲染带变量的聊天提示词。
  • 理解 Agent prompt 应该负责什么,不应该负责什么。
  • 判断哪些问题不能只靠改 prompt 解决。

Message 是 Agent 的上下文载体

聊天模型看到的不是“一个神秘的提示词字符串”,而是一组有角色的 message。Agent 的上下文、用户问题、模型中间回复、工具调用结果,最终都会被组织成 message 列表交给模型。

常见 message 类型包括:

  • System message:定义模型的角色、边界、输出格式和重要规则。它通常放在最前面,用来影响整轮任务的行为。
  • Human message:用户输入的问题、指令或补充信息。
  • AI message:模型已经生成过的回复,也可能包含工具调用请求。
  • Tool message:工具执行后的结果。它告诉模型某次工具调用返回了什么。

Agent 能不能稳定运行,很大程度取决于这些 message 是否清晰、顺序是否正确、工具结果是否可读。Prompt 不是孤立文本,而是 message 结构的一部分。

PromptTemplate

PromptTemplate 用来把变量渲染进提示词。对于聊天模型,更常用的是 ChatPromptTemplate,因为它可以直接构造多条带角色的 message。

运行本章示例:

bash
uv run python examples/02_prompt_cli.py

示例会把用户问题渲染到 human message 中,并先打印渲染后的 messages,再调用模型:

python
prompt = ChatPromptTemplate.from_messages(
    [
        ("system", "你是一名严谨的 LangChain 教练。请用中文回答,并把回复分成:核心概念、最小示例、常见误区。"),
        ("human", "{question}"),
    ]
)

rendered_prompt = prompt.invoke({"question": question})
messages = rendered_prompt.to_messages()
response = model.invoke(messages)

这里使用 tuple message templates,而不是把 {question} 写进普通 HumanMessage 对象。普通 message 对象通常代表已经确定的内容,不一定会参与模板渲染;tuple 模板能明确告诉 LangChain 哪些字段需要替换。

Agent Prompt 的边界

Agent prompt 应该帮助模型理解任务边界和协作方式,例如:

  • 当前 Agent 的角色是什么。
  • 可以使用哪些信息源和工具。
  • 什么时候应该调用工具,什么时候可以直接回答。
  • 输出格式应该如何组织。
  • 不确定时应该承认不知道,还是继续检索或询问用户。

但是 Agent prompt 不应该承担所有系统责任。Prompt 可以约束模型行为,却不能替代工具实现、数据质量、状态管理和可观测性。

一个实用边界是:prompt 负责“告诉模型应该怎样思考和表达”,代码负责“保证工具、上下文、检索、权限和错误处理按预期工作”。

常见误区

误区一:把所有问题都归因于 prompt。

如果工具 schema 不清楚、参数类型不合理、工具返回值难以理解,模型可能无法稳定调用工具。继续堆提示词通常只会让上下文更长,问题并不会消失。

误区二:认为 prompt 可以修复模型能力不足。

某些本地模型能聊天,但不擅长严格 JSON、工具调用或长上下文推理。Prompt 可以改善表现,但不能把不可靠能力变成可靠系统。需要通过模型选择、工具设计和测试共同解决。

误区三:认为 prompt 可以修复 RAG 数据问题。

如果检索没有找回正确资料,或者切片把关键上下文拆散,模型拿不到事实依据。此时应该检查数据清洗、chunk 策略、embedding、检索参数和重排逻辑,而不是只改系统提示词。

误区四:忽略历史消息和工具消息。

Agent prompt 只是上下文的一部分。真实 Agent 运行时,历史 AI message、tool call 和 tool result 都会影响下一步决策。调试时要打印完整 messages,而不是只看最初的 system prompt。

代码阅读重点

阅读 examples/02_prompt_cli.py 时,重点看 ChatPromptTemplate.from_messages()

这个模板把消息分成两层:

  • system 消息:稳定规则,告诉模型应该扮演什么角色,回答格式是什么。
  • human 消息:用户问题,通过 {question} 注入。

运行脚本时会先打印渲染后的 messages,再打印模型回答。这个设计是故意的:调试 Prompt 时,不要只看模板源码,要看最终传给模型的消息。

Prompt 调试方法

当回答不符合预期时,可以按下面顺序排查:

  1. 最终 messages 是否包含你以为传进去的变量?
  2. system 消息是否太长,导致核心规则不突出?
  3. 用户问题是否和系统规则冲突?
  4. 输出格式是否要求过细,超出了本地模型的稳定能力?
  5. 是否应该把格式要求拆成更简单的步骤?

Prompt 调试不是不断堆规则。规则越多,模型越可能忽略某些约束。更稳的做法是把关键规则写清楚,把可验证的事情交给程序检查。

实验练习

  • 把系统消息中的“三部分回答”改成“两部分回答”,观察输出变化。
  • 删除系统消息,只保留 human 消息,比较回答是否更发散。
  • 把默认问题改成一个非 LangChain 问题,观察系统角色对回答的约束有多强。

自测问题

  • System Message 和 Human Message 的职责有什么不同?
  • PromptTemplate 的价值只是字符串拼接吗?
  • 为什么调试 Agent 前要先看渲染后的 messages?

完整示例

python
from __future__ import annotations

import sys
from pathlib import Path

sys.path.append(str(Path(__file__).resolve().parents[1]))

from langchain_core.prompts import ChatPromptTemplate
from langchain_ollama import ChatOllama

from examples.common.config import load_config, ollama_client_kwargs
from examples.common.ollama_check import assert_model_available, assert_ollama_running


def main() -> None:
    config = load_config()
    try:
        assert_ollama_running(config.ollama_base_url)
        assert_model_available(config.ollama_base_url, config.ollama_model)
    except RuntimeError as exc:
        print(f"错误:{exc}")
        return

    model = ChatOllama(
        model=config.ollama_model,
        base_url=config.ollama_base_url,
        client_kwargs=ollama_client_kwargs(),
        temperature=0,
    )
    prompt = ChatPromptTemplate.from_messages(
        [
            (
                "system",
                "你是一名严谨的 LangChain 教练。请用中文回答,并把回复分成:"
                "核心概念、最小示例、常见误区。",
            ),
            ("human", "{question}"),
        ]
    )

    default_question = "LangChain 的 Agent 是什么?"
    question = input(f"问题(默认:{default_question}):").strip() or default_question

    rendered_prompt = prompt.invoke({"question": question})
    messages = rendered_prompt.to_messages()

    print("\n渲染后的 messages:")
    for message in messages:
        print(f"- {message.type}: {message.content}")

    response = model.invoke(messages)
    print(f"\nAI: {response.content}")


if __name__ == "__main__":
    main()