前置地基:LLM API 与 Prompt 基础

实战锚点 · cooking-app:chat completions、流式、原生 Function Calling 的 JSON 形态;system/user 角色、结构化输出、few-shot——Dify 帮你藏起来的那层。

在碰任何框架之前,必须先看懂裸 API 长什么样——因为 Dify/LangChain 全部是对这一层的包装。教材就用真实代码:我桌面 cooking-app 项目的 AI 服务(NestJS + DeepSeek,已上线运行)。

读一段真实的生产代码

cooking-app/backend/src/ai/ai.service.ts(节选,真实文件):

private async callDeepSeek(messages: any[], context?: any): Promise<any> {
  const requestBody = {
    model: this.deepseekModel,                       // "deepseek-chat"
    messages: [
      { role: "system", content: systemPrompt },     // 人设与规则
      ...messages.map(msg => ({ role: msg.role, content: msg.content })),
    ],
    max_tokens: 2000,
    temperature: 0.7,
  };

  const response = await fetch(`${this.deepseekApiUrl}/chat/completions`, {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${this.deepseekApiKey}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify(requestBody),
  });

  const data = await response.json();
  return { content: data.choices[0].message.content, usage: data.usage };
}

这 30 行就是 LLM API 的全部本质:一个 POST 请求,发消息数组,拿回续写。DeepSeek/Qwen/GPT 全部兼容这个 OpenAI 协议格式。后面所有框架解决的都是这段代码没有的东西。

消息数组:唯一的输入格式

messages: [
  { role: "system",    content: "你是烹饪助手…规则…" }  // 人设/规则/约束(权重高)
  { role: "user",      content: "鸡胸肉怎么做嫩?" }     // 用户输入
  { role: "assistant", content: "上一次回答…" }          // 历史回答
  { role: "user",      content: "那低油版本呢?" }       // 新输入
]

关键认知:模型没有"记忆"。所谓多轮对话,就是每次把全部历史重新发一遍(对,每一轮都在为全部历史付 token 账单)。"记忆管理"就是决定哪些历史留、哪些摘要、哪些扔——后面 LangChain 篇的 Memory 组件干的就是这个。

System Prompt:写好它的四个要素

cooking-app 的 system prompt 真实结构:

你是一个专业的烹饪助手…(角色)
你的任务是帮助用户解决烹饪相关的问题,包括:
1. 菜谱推荐和解释  2. 烹饪技巧指导  3. 食材替代建议…(职责清单)
【回答要求】
- 回答简洁实用,先给结论再给步骤
- 不确定时如实说明,不要编造(约束/兜底)
要素作用缺了会怎样
角色定语气和知识域回答像百科不像助手
职责清单划定回答范围什么都能聊,包括不该聊的
输出要求控制格式与长度长篇大论
兜底约束不确定时承认幻觉放大

few-shot 比抽象指令有效:与其写"回答要简洁",不如直接给 1-2 个理想问答示例——模型模仿示例的能力远强于理解形容词的能力。

流式输出:用户体感的分水岭

裸代码没做流式(这是它真实的历史局限)。流式的本质极其简单——响应是一块块到的:

const response = await fetch(url, {
  method: "POST",
  body: JSON.stringify({ ...requestBody, stream: true }),  // 就加这一个参数
  headers: { Authorization: `Bearer ${key}` },
});

const reader = response.body!.getReader();
const decoder = new TextDecoder();
while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  const chunk = decoder.decode(value);          // "data: {...}\n\ndata: {...}"
  // 每块解析出 delta.content,立刻推给前端
}

为什么模型"天生适合"流式?因为它本来就是逐 token 生成的——完整答案是等它全部写完,流式是它写一句你听一句。前端把 chunk 推给 SSE/WebSocket,就是 ChatGPT 那种打字机效果(你的前端技能在这里直接变现)。

结构化输出:让模型输出可解析的 JSON

让模型"输出 JSON"是应用开发最高频需求。两种姿势:

// 姿势一:prompt 约束(能懂但不可靠,模型偶尔夹带说明文字)
{ role: "system", content: "只输出 JSON,格式:{name, minutes, steps[]}" }

// 姿势二:response_format / withStructuredOutput(协议级强约束,推荐)
const body = {
  model: "deepseek-chat",
  messages: [...],
  response_format: { type: "json_object" },   // DeepSeek 支持 JSON mode
};

框架层会更进一步:llm.withStructuredOutput(zodSchema) 直接给你类型安全的对象(TS 的强项,LangChain 篇见)。

Function Calling 的裸形态(预告)

模型不能执行代码,但它能输出"我想调用什么工具":

{
  "choices": [{
    "message": {
      "content": null,
      "tool_calls": [{
        "function": { "name": "get_recipe", "arguments": "{\"ingredient\":\"鸡胸肉\"}" }
      }]
    }
  }]
}

你的代码拿到这个 JSON → 真的去查菜谱 → 把结果以 role: "tool" 消息塞回数组 → 再调一次 → 循环直到模型给出自然语言答案。这个"调用→执行→回填→再调用"的循环就是 Agent 的心脏。LangChain 篇的 lc-tools 章会把它完整实现一遍。

本章自查

  • 能默写 messages 四种 role 和各自用途
  • 能解释"多轮对话为什么每轮都付全款"
  • system prompt 四要素能对号入座
  • 知道流式只是 stream: true + 逐块读
  • 能说清 FC 循环的五步(调用→执行→回填→再调→终止)

下一章

地基打完,上平台——20 分钟跑通第一个应用,看看 Dify 把这些裸代码包装成了什么样。