Skip to content

从零到进阶做 Agent(三)提示词工程

🕒 Published at:

前两篇我们把模型「调起来」了。但真正决定效果的,往往不是代码,而是你怎么对模型说话。这一篇讲提示词工程:把任务说清楚、让模型稳定吐出可被程序解析的 JSON,并用两个金融领域的实战案例(文本分类、信息抽取)串起来。代码只用上一篇的 openai SDK,零新依赖,顶部可切换 Python / Node.js。

一个好提示词的四要素

给模型下达任务时,越明确越稳定。一个可靠的提示词通常包含:角色(你是谁)、任务(做什么)、约束(规则、边界、输出格式)、示例(给一两个范例,也就是 few-shot)。其中「输出格式」对做应用尤其关键——因为程序要接收模型的输出继续处理,我们需要的不是一段散文,而是结构化数据

让模型稳定输出 JSON

如果只在提示词里说「请返回 JSON」,模型偶尔会夹带解释文字或用 ```json 代码块包裹,导致 JSON.parse 失败。稳妥的做法有两层:一是提示词里明确「只返回 JSON,不要任何多余文字」;二是打开接口的 JSON 模式response_format),强制输出合法 JSON。注意:使用 JSON 模式时,提示词里必须出现 “json” 字样,这是接口的硬性要求。

python
import os, json
from openai import OpenAI

client = OpenAI(
    api_key=os.getenv("DASHSCOPE_API_KEY"),
    base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
)

def ask_json(system: str, user: str) -> dict:
    resp = client.chat.completions.create(
        model="qwen-plus",
        messages=[
            {"role": "system", "content": system},
            {"role": "user", "content": user},
        ],
        response_format={"type": "json_object"},  # 强制返回合法 JSON
        temperature=0,                            # 抽取/分类类任务用 0 更稳定
    )
    return json.loads(resp.choices[0].message.content)

data = ask_json(
    system="你是信息抽取助手,只输出 json,不要任何多余文字。",
    user='从这句话抽取城市和温度,返回 json:{"city":..., "temp":...}。原文:今天杭州 28 度。',
)
print(data, type(data))
javascript
import OpenAI from "openai";

const client = new OpenAI({
  apiKey: process.env.DASHSCOPE_API_KEY,
  baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
});

async function askJson(system, user) {
  const resp = await client.chat.completions.create({
    model: "qwen-plus",
    messages: [
      { role: "system", content: system },
      { role: "user", content: user },
    ],
    response_format: { type: "json_object" },  // 强制返回合法 JSON
    temperature: 0,                            // 抽取/分类类任务用 0 更稳定
  });
  return JSON.parse(resp.choices[0].message.content);
}

const data = await askJson(
  "你是信息抽取助手,只输出 json,不要任何多余文字。",
  '从这句话抽取城市和温度,返回 json:{"city":..., "temp":...}。原文:今天杭州 28 度。'
);
console.log(data, typeof data);

temperature=0 让输出尽量确定,这对分类、抽取这类「有标准答案」的任务很重要;而 response_format 保证了拿到的一定是能 parse 的合法 JSON。这两点是把大模型接进业务系统的基础。

案例一:金融文本分类

把一段财经新闻标题自动归类到「利好 / 利空 / 中性」。要点是在提示词里固定候选标签,并要求模型只从中选择,避免它自由发挥出奇怪的类别。

python
def classify(title: str) -> dict:
    system = (
        "你是金融舆情分类助手。请判断新闻对相关公司股价的影响,"
        "只能从 [利好, 利空, 中性] 三个标签中选一个。"
        "只输出 json,格式:{\"label\": \"利好/利空/中性\", \"reason\": \"简短理由\"}。"
    )
    return ask_json(system, f"新闻标题:{title}")

for t in ["某公司季度净利润同比大增 120%", "监管部门对某银行开出千万罚单"]:
    print(t, "=>", classify(t))
javascript
async function classify(title) {
  const system =
    "你是金融舆情分类助手。请判断新闻对相关公司股价的影响," +
    "只能从 [利好, 利空, 中性] 三个标签中选一个。" +
    '只输出 json,格式:{"label": "利好/利空/中性", "reason": "简短理由"}。';
  return askJson(system, `新闻标题:${title}`);
}

for (const t of ["某公司季度净利润同比大增 120%", "监管部门对某银行开出千万罚单"]) {
  console.log(t, "=>", await classify(t));
}

案例二:金融信息抽取(含 few-shot)

从一段研报文字里抽取结构化字段(公司、评级、目标价)。这类任务给一个示例能显著提升稳定性——这就是 few-shot:把「输入→输出」的样例放进提示词,让模型照着格式套。

python
def extract(report: str) -> dict:
    system = (
        "你是研报信息抽取助手,只输出 json。"
        "字段:company(公司名), rating(评级), target_price(目标价, 数字, 没有则 null)。\n"
        "示例:\n"
        "输入:我们首次覆盖 A 公司,给予买入评级,目标价 45 元。\n"
        '输出:{"company": "A公司", "rating": "买入", "target_price": 45}'
    )
    return ask_json(system, f"输入:{report}\n输出:")

print(extract("维持 B 公司增持评级,上调目标价至 88.5 元。"))
javascript
async function extract(report) {
  const system =
    "你是研报信息抽取助手,只输出 json。" +
    "字段:company(公司名), rating(评级), target_price(目标价, 数字, 没有则 null)。\n" +
    "示例:\n" +
    "输入:我们首次覆盖 A 公司,给予买入评级,目标价 45 元。\n" +
    '输出:{"company": "A公司", "rating": "买入", "target_price": 45}';
  return askJson(system, `输入:${report}\n输出:`);
}

console.log(await extract("维持 B 公司增持评级,上调目标价至 88.5 元。"));

一点工程经验

即便开了 JSON 模式,健壮的代码也应该对 parse 做 try/catch,并在失败时重试或降级——大模型终究有概率不按预期来。字段越少、指令越具体、给了示例,稳定性越高。真正上生产时,还会把「预期结构」用 JSON Schema 或校验库(Python 的 pydantic、Node 的 zod)再校验一遍,这也是下一篇 LangChain 的 OutputParser 要帮我们做的事。

小结与预告

这一篇的核心就一句话:把输出结构化。你学会了用 response_format 稳定拿 JSON、用固定标签做分类、用 few-shot 做抽取。这些技巧和框架无关,是所有 LLM 应用的地基。

但你可能已经感觉到:提示词、解析、重试、模板拼接……手写会越来越啰嗦。下一篇《LangChain 入门》,我们请出专门的框架来收拾这些重复劳动——用 PromptTemplate 管模板、用 LCEL 把「模板→模型→解析」串成一条链、用 OutputParser 自动解析输出。