你肯定用过ChatGPT或Claude的网页版,也可能调用过几次API。但你有没有想过一个问题:
同样是调用gpt-4模型,为什么:
- 你在ChatGPT网页上问"帮我写代码",它会给你一段代码 + 详细解释
- 但如果你用API调用,只发送
{"role": "user", "content": "帮我写代码"},它可能回复"好的,请告诉我你想写什么代码"
区别在哪?
答案藏在一个你看不见的地方:System Prompt(系统提示词)。
💡 回到第一篇的"实习生"比喻 :System Prompt就是实习生的岗位说明书。
ChatGPT网页版在你看不见的地方,已经偷偷给实习生塞了一份说明书,上面写着:
- 你是一个有用的助手
- 你应该提供详细的解释
- 你应该用Markdown格式输出代码
而你直接调用API时,实习生没有拿到岗位说明书,它站在你面前,一脸茫然地问:"老板,我该干什么?"
这一篇,我们就要揭开这个"看不见的开关",并且亲手写一个能让模型"听话干活"的System Prompt。
读完这篇文章,你将:
- 理解System Prompt的本质:它不是"礼貌用语",而是控制模型行为的核心机制
- 掌握API调用的完整流程:从环境变量到消息格式,写出第一个能跑通的LLM调用
- 学会设计Agent专用的System Prompt:让模型输出结构化的Thought和Action,而不是随意聊天
在动手写代码前,我们先理解一个核心公式。
模型输出 = f(System Prompt, 对话历史, 温度参数, ...)
用人话翻译:
- System Prompt:告诉模型"你是谁,该怎么做"(角色设定)
- 对话历史:之前的所有对话内容(上下文)
- 温度参数:控制输出的随机性(0=确定性,1=创造性)
关键洞察 :System Prompt是这个公式里唯一一个你可以完全控制、且对所有对话都生效的变量。
回忆第一篇文章中我们建立的ReAct公式:
初始状态 S_0 = (用户指令, 空对话历史) 循环 t = 0, 1, 2, ...: Thought_t, Action_t = LLM(S_t) ← 📍 本篇解决的就是这部分! Observation_t = Execute(Action_t) S_{t+1} = S_t + (Thought_t, Action_t, Observation_t)
这一篇聚焦于LLM(S_t):如何让模型根据当前状态,输出我们想要的格式(Thought + Action)。
System Prompt就是控制这个LLM(S_t)输出的"旋钮"------它告诉模型:
- 你应该先思考(输出Thought)
- 再给出行动(输出Action)
- 格式必须是:
Thought: ... Action: ...
下一篇我们会实现Execute(Action_t),把Action真正执行。
回到我们的"实习生工具箱"类比:
- 没有System Prompt的模型 = 一个没有岗位说明书的实习生,你每次都要重复告诉他"你是工程师,要写代码"
- 有System Prompt的模型 = 一个拿到了岗位说明书的实习生,他知道自己的职责,你只需要说"帮我修Bug",他就知道该怎么做
让我们看一个真实的对比:
实验A:普通聊天助手
System Prompt: "你是一个有用的助手。"
User: "怎么读取文件?" Model: "读取文件有很多方法,你想用哪种编程语言呢?Python的话可以用open()函数…"
实验B:工程师Agent
System Prompt: """你是一个Python工程师Agent。
当用户提问时,你应该:
- 先思考(输出Thought)
- 再给出代码(输出Action) 输出格式: Thought: [你的思考过程] Action: [具体的代码] """ User: "怎么读取文件?" Model: """ Thought: 用户想知道如何读取文件,我应该给出Python的标准做法,使用with语句确保文件正确关闭。 Action:
”`python with open(‘file.txt’, ‘r’) as f:
content = f.read()
"""
看到区别了吗?
- 实验A:模型在"聊天",输出是自然语言对话
- 实验B:模型在"工作",输出是结构化的思考+代码
这就是System Prompt的威力——它决定了模型的"工作模式"。
动手实操:封装你的第一个LLMClient
现在我们开始写代码。目标是封装一个极简的LLMClient类,能够:
- 读取API Key(从环境变量)
- 发送消息给模型
- 返回模型的回复
第一步:安装依赖和设置API Key
”`bash pip install openai
然后设置环境变量(千万别硬编码在代码里!):
# Linux/Mac
export OPENAI_API_KEY="sk-…"
Windows PowerShell
$env:OPENAI_API_KEY="sk-…"
或者用.env文件(推荐)
echo ‘OPENAI_API_KEY=sk-…’ > .env
创建一个文件llm_client.py:
import os
from openai import OpenAI
class LLMClient:
"""极简的LLM客户端,封装API调用""" def __init__(self, model="gpt-4"): # 🔑 从环境变量读取API Key,避免硬编码 api_key = os.getenv("OPENAI_API_KEY") if not api_key: raise ValueError("请设置环境变量 OPENAI_API_KEY") self.client = OpenAI(api_key=api_key) self.model = model def chat(self, messages): """ 发送消息给模型,返回回复内容 参数: messages: 消息列表,格式为 [{"role": "system/user/assistant", "content": "..."}] 返回: 模型的回复文本 """ # 🔑 核心:messages是一个消息列表,每条消息有role(角色)和content(内容) # role有三种: # - "system":系统提示词,设定模型的行为规则 # - "user":用户说的话 # - "assistant":模型之前的回复(用于多轮对话) # # 注意:messages是一个列表,模型会按顺序读取, # 所以顺序很重要!System Prompt应该放在第一条。 response = self.client.chat.completions.create( model=self.model, messages=messages, temperature=0.7 # 0=确定输出(适合代码),1=随机输出(适合创意) ) # 🔑 提取模型回复的文本内容 return response.choices[0].message.content
代码解读:
__init__:初始化时读取API Key,如果没有就报错(避免运行到一半才发现)chat:核心方法,接收消息列表,返回模型回复messages:这是OpenAI API的标准格式,每条消息都有role(角色)和content(内容)
创建一个测试文件test_basic.py:
from llm_client import LLMClient
创建客户端
client = LLMClient(model="gpt-4")
🔑 构造消息列表
messages = [
{ "role": "system", "content": "你是一个Python专家,回答要简洁专业。" }, { "role": "user", "content": "用一句话介绍你自己。" }
]
调用模型
response = client.chat(messages) print("模型回复:") print(response)
运行这段代码,你会看到类似这样的输出:
模型回复:
我是一个Python专家,专注于提供简洁、专业的技术解答和代码实现。
恭喜!你已经成功调用了LLM API。
现在我们做一个对比实验,验证System Prompt的作用。创建test_comparison.py:
from llm_client import LLMClient
client = LLMClient(model="gpt-4")
实验A:普通助手
print("=" * 50) print("实验A:普通助手") print("=" * 50) messages_a = [
{"role": "system", "content": "你是一个有用的助手。"}, {"role": "user", "content": "怎么读取文件?"}
] response_a = client.chat(messages_a) print(response_a)
实验B:工程师Agent
print(" " + "=" * 50) print("实验B:工程师Agent") print("=" * 50) messages_b = [
{ "role": "system", "content": """你是一个Python工程师Agent。
当用户提问时,你应该:
- 先思考(输出Thought)
- 再给出代码(输出Action)
输出格式: Thought: [你的思考过程] Action: [具体的代码] """
}, {"role": "user", "content": "怎么读取文件?"}
] response_b = client.chat(messages_b) print(response_b)
运行后你会看到明显的差异:
实验A可能输出:
读取文件有多种方法,最常用的是使用Python的open()函数。你可以这样做:
[一段代码 + 详细解释]
实验B可能输出:
Thought: 用户需要读取文件的代码示例,我应该给出使用with语句的标准做法。
Action: with open(‘file.txt’, ‘r’) as f:
content = f.read()
看到了吗?同样的问题,不同的System Prompt,输出的结构完全不同。
在真实的Claude Code实现中(rust版本),这部分对应的是:
LLMClient.chat()
crates/api/src/client.rs 的
ProviderClient trait 真实版支持流式输出、多Provider
messages 格式
crates/api/src/types.rs 的消息结构体 真实版有更多字段(stop、top_p等) System Prompt 在
crates/runtime/src/prompt.rs中动态生成 真实版长达数百行,包含工具定义
想深入研究的读者:
- 打开
crates/api/src/client.rs,搜索trait ProviderClient,你会看到流式处理的完整逻辑 - 打开
crates/tools/src/lib.rs,可以看到工具注册和执行的机制
为什么我们的实现这么简单?
记住我们的目标:理解齿轮怎么转,而不是造一辆完整的车。
真实代码还包括:
- 流式输出(逐字返回,而不是等全部生成完)
- 多Provider支持(OpenAI、Anthropic、本地模型)
- 错误重试机制
- Token计数和预算管理
但核心逻辑是一样的:构造消息列表 → 调用API → 返回结果。
通过上面的实验,我们总结出设计Agent专用System Prompt的3个原则:
❌ 不好的写法:
你是一个助手。
✅ 好的写法:
你是一个Python工程师Agent,专门帮助用户修复代码Bug、编写测试、优化性能。
为什么? 明确的角色定位让模型知道自己的"专业领域",避免答非所问。
❌ 不好的写法:
你应该帮助用户解决问题。
✅ 好的写法:
你的输出格式必须是:
Thought: [你的思考过程] Action: [具体的操作]
为什么? 结构化的输出格式让我们能够解析模型的回复,提取出"思考"和"行动"两部分。这是ReAct循环的基础。
❌ 不好的写法:
你应该先思考再行动。
✅ 好的写法:
示例:
User: 帮我读取main.py文件 Assistant: Thought: 用户想看main.py的内容,我应该使用read_file工具。 Action: read_file(‘main.py’)
为什么? 具体示例是"Few-shot Learning"的体现,让模型通过例子理解你的期望。
- 坑1:API Key硬编码在代码里
- 后果:代码一旦上传到GitHub,Key就泄露了,可能被盗刷
- 正确做法:用环境变量或
.env文件
- 坑2:忘记设置System Prompt
- 后果:模型不知道自己该扮演什么角色,输出不可控
- 正确做法:每次调用都要包含System Prompt
- 坑3:System Prompt写得太模糊
- 错误示例:"你是一个好助手"
- 后果:模型不知道"好"的标准是什么
- 正确做法:具体描述角色、输出格式、工作方式
- 坑4:温度参数设置不当
temperature=0:输出非常确定,适合代码生成temperature=1:输出很随机,适合创意写作- Agent通常用
0.3-0.7之间
现在你已经学会了:
- 封装LLM API调用
- 设计System Prompt让模型输出结构化内容
- 理解System Prompt对模型行为的控制力
但有一个关键问题还没解决:
模型现在只会"说"(输出Thought和Action的文本),但不会"做"(真正执行Action)。
比如,模型输出了:
Thought: 我需要读取main.py
Action: read_file(‘main.py’)
但这只是一段文本,文件并没有真的被读取。
下一篇,我们将实现ReAct循环的骨架------让模型能够:
- 输出Action
- 我们解析这个Action
- 执行对应的工具
- 把结果返回给模型
- 模型根据结果继续思考
这就是Agent从"聊天机器人"进化到"能干活的助手"的关键一步。
预告一个核心问题 :模型输出的是自然语言文本"read_file('main.py')",我们怎么把它转换成真正的Python函数调用?答案在下一篇揭晓。
在进入下一篇之前,请确认你能回答以下问题:
如果都能回答,恭喜你,Agent的"大脑"部分你已经掌握了。下一篇见!
系列进度
- ✅ 第1篇:总览与前置准备——Claude Code到底是什么?
- ✅ 第2篇:地基篇——让模型开口说话(System Prompt的艺术)
- ⏭️ 第3篇:灵魂篇——ReAct循环的骨架
- 第4篇:双手篇——赋予读写文件的能力
- 第5篇:终端篇——赋予执行命令的超能力
- 第6篇:整合篇——组装Mini Claude Code
- 第7篇:上下文篇——让Agent看懂整个文件夹
- 第8篇:反思与展望——我们得到了什么,还缺什么?
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/280944.html