8年Java开发者AI转型第一周:从零搭建RAG文档问答系统(Day 3-4)

0 阅读11分钟

8年Java开发者AI转型第一周:从零搭建RAG文档问答系统(Day 3-5)

系列:8年Java开发者AI转型第一周
上一篇Day 1-2:Python速学 + 环境搭建
本期内容:OpenAI API / 国内大模型API调用、Chat Completion结构、Prompt工程、参数调优、Few-shot


gitHub

开发转型,从"Java开发"到"AI应用开发"的核心技能储备

一、背景

上一篇我们搭好了 Python 环境,简单的了解了Python基础和java的区别。

这一期,我们看看:LLM 的 API 到底是什么样子的?request 和 response 的结构是怎样的?system prompt、user prompt 到底有什么区别?temperature、top_p 这些参数调了会怎样?

这些知识,是你后续做任何 AI 应用(RAG、Agent、微调……)的地基


二、环境搭建

2.1 安装 OpenAI Python SDK

不管你调的是 OpenAI 还是 DeepSeek 还是智谱 GLM,它们都遵循 OpenAI 兼容的 API 协议。所以只需要装一个 SDK:

pip install openai

2.2 配置文件

参考 config_bigmodel.json,我们把不同模型的配置分开:

{
    "deepseek": {
        "api_key": "sk-xxxxx",
        "base_url": "https://api.deepseek.com/v1"
    },
    "zhipu": {
        "api_key": "xxxxx",
        "base_url": "https://open.bigmodel.cn/api/paas/v4"
    },
    "openai": {
        "api_key": "sk-xxxxx",
        "base_url": "https://api.openai.com/v1"
    }
}

注意base_url 是关键。OpenAI 官方是 https://api.openai.com/v1,DeepSeek 是 https://api.deepseek.com/v1,智谱是 https://open.bigmodel.cn/api/paas/v4。SDK 是一样的,只是 URL 不同。


三、Chat Completion API 的 Request / Response 结构

3.1 最简单的调用

先看一个最基础的请求:

from openai import OpenAI
import json

# 从配置文件读取
with open("config_bigmodel.json", "r", encoding="utf-8") as f:
    config = json.load(f)

client = OpenAI(
    api_key=config["deepseek"]["api_key"],
    base_url=config["deepseek"]["base_url"]
)

# 发送请求
response = client.chat.completions.create(
    model="deepseek-v4-pro",
    messages=[
        {"role": "user", "content": "你好,介绍一下你自己"}
    ]
)

print(response.choices[0].message.content)

3.2 Request 结构拆解

上面那段代码,实际发出的 HTTP 请求大致是这样的:

{
    "model": "deepseek-v4-pro",
    "messages": [
        {
            "role": "user",
            "content": "你好,介绍一下你自己"
        }
    ]
}

核心字段只有两个:

  • model:指定用哪个模型
  • messages:消息列表,数组中的每条消息都有 rolecontent

3.3 Response 结构拆解

模型返回的 JSON 结构:

{
    "id": "chatcmpl-xxx",
    "object": "chat.completion",
    "created": 1720000000,
    "model": "deepseek-v4-pro",
    "choices": [
        {
            "index": 0,
            "message": {
                "role": "assistant",
                "content": "你好!我是 DeepSeek..."
            },
            "finish_reason": "stop"
        }
    ],
    "usage": {
        "prompt_tokens": 15,
        "completion_tokens": 50,
        "total_tokens": 65
    }
}

代码里我们只取了 response.choices[0].message.content,但其实还有很多有价值的信息:

字段含义用途
response.id请求ID排查问题时向客服提供
response.choices[0].finish_reason停止原因"stop"=正常结束,"length"=超出max_tokens
response.usage.prompt_tokens输入token数计费依据
response.usage.completion_tokens输出token数计费依据

四、System / User / Assistant 的角色区别

这是很多人最容易混淆的地方。messages 数组里的每条消息都有一个 role,它决定了这条消息是谁说的

4.1 三种角色的含义

role含义谁能发类比
system系统指令,给模型定规矩只能是开发者老板给员工定的"工作手册"
user用户的提问或指令可以是用户或开发者员工接到的"工作任务"
assistant模型的回复只能是模型(但你可以预填)员工的"工作成果"

4.2 实际代码示例

response = client.chat.completions.create(
    model="deepseek-v4-pro",
    messages=[
        # system: 给模型设定角色和行为边界
        {"role": "system", "content": "你是一个专业的Python代码审查专家。你的职责是找出代码中的bug、性能问题和安全漏洞。回答时要指出具体问题位置,并给出修改建议。"},
        
        # user: 用户的问题
        {"role": "user", "content": "请审查以下代码:\n\ndef calculate_sum(numbers):\n    total = 0\n    for i in range(len(numbers)):\n        total += numbers[i]\n    return total"},
    ]
)

print(response.choices[0].message.content)

输出可能是:

这段代码功能是计算列表元素之和,虽然逻辑正确,但存在以下问题:

1. 【性能问题】使用索引遍历效率较低
   建议改为:for num in numbers: total += num
   或直接使用内置函数:return sum(numbers)

2. 【安全隐患】没有输入校验
   如果传入非列表类型会抛出TypeError
   建议添加类型检查...

4.3 多轮对话:消息是怎么累积的

LLM 没有记忆,每一轮对话都需要你把历史消息全部传回去:

messages = [
    {"role": "system", "content": "你是一个数学老师"},
]

# 第一轮
messages.append({"role": "user", "content": "1+1等于几?"})
resp1 = client.chat.completions.create(model="deepseek-v4-pro", messages=messages)
messages.append(resp1.choices[0].message)  # 把模型的回复加入历史

# 第二轮
messages.append({"role": "user", "content": "那2+3呢?"})
resp2 = client.chat.completions.create(model="deepseek-v4-pro", messages=messages)
print(resp2.choices[0].message.content)
# 输出: 2+3等于5

关键点messages 数组会越来越大,这意味着token消耗越来越多,费用也越来越高。实际项目中要注意控制对话历史长度。


五、参数调优:Temperature、Top-p、Max Tokens

这三个参数直接决定模型的输出风格,是 Prompt Engineering 的核心。

5.1 Temperature — 随机性控制

温度越高,输出越"放飞自我";温度越低,输出越"一本正经"。

# 低温度:严谨、确定性强
response_low = client.chat.completions.create(
    model="deepseek-v4-pro",
    messages=[{"role": "user", "content": "写一首关于春天的诗"}],
    temperature=0.1   # 几乎每次输出都一样
)

# 高温度:创意、多样性强
response_high = client.chat.completions.create(
    model="deepseek-v4-pro",
    messages=[{"role": "user", "content": "写一首关于春天的诗"}],
    temperature=0.9   # 每次输出都可能不一样
)
temperature 值效果适用场景
0.0 ~ 0.3高度确定性,几乎不变代码生成、事实问答、数据分析
0.4 ~ 0.7适度创意通用对话、摘要、翻译
0.8 ~ 1.0高度创意,可能胡说头脑风暴、故事创作

建议:RAG 场景下用 0.0~0.3,因为你要的是准确回答,不是创意。

5.2 Top-p — 核采样阈值

Top-p 控制模型从哪些词里面选下一个词。

response = client.chat.completions.create(
    model="deepseek-v4-pro",
    messages=[{"role": "user", "content": "明天天气怎么样?"}],
    top_p=0.9   # 只从累计概率90%的词里面选
)
top_p 值效果
0.1只选最可能的10%的词 → 输出非常保守
0.5选最可能的50%的词 → 适度多样
0.9选最可能的90%的词 → 比较开放
1.0所有词都参与 → 最开放

Temperature 和 Top-p 的关系

  • 两者可以同时设置,但通常只需要设一个
  • Temperature 控制的是"概率分布的形状",Top-p 控制的是"截断哪些词"
  • 推荐做法:temperature=0.7, top_p=0.9 是一个不错的默认组合

5.3 Max Tokens — 最大输出长度

response = client.chat.completions.create(
    model="deepseek-v4-pro",
    messages=[{"role": "user", "content": "请详细介绍RAG的原理"}],
    max_tokens=2048  # 最多输出2048个token
)
max_tokens 值适用场景
100~300简短回答、分类、提取
500~1000一般问答、摘要
2000~4096详细解释、代码生成
8192+长文档生成

注意:如果输出被截断(finish_reason="length"),说明 max_tokens 不够,需要增大。

5.4 综合调优示例

response = client.chat.completions.create(
    model="deepseek-v4-pro",
    messages=[
        {"role": "system", "content": "你是一个资深的Java转Python的技术导师,擅长用Java类比讲解Python概念。"},
        {"role": "user", "content": "请用Java的概念帮我理解Python的装饰器"}
    ],
    temperature=0.3,      # 偏严谨,因为要准确解释概念
    top_p=0.9,            # 允许一定的表达多样性
    max_tokens=1024,       # 足够长的回答
    stream=False           # 非流式,一次性返回
)

print(response.choices[0].message.content)
print(f"消耗tokens: {response.usage.total_tokens}")

六、流式输出(Streaming)

上面的例子都是 stream=False,模型憋完整个答案才一次性返回。但实际产品中,我们更希望边想边说,像打字机一样逐字输出。

6.1 非流式 vs 流式对比

# 非流式:等模型全部生成完才返回
response = client.chat.completions.create(
    model="deepseek-v4-pro",
    messages=[{"role": "user", "content": "写一篇2000字的技术文章"}],
    stream=False
)
print(response.choices[0].message.content)  # 一次性拿到全部内容
# 流式:模型每生成一部分就推送一次
response = client.chat.completions.create(
    model="deepseek-v4-pro",
    messages=[{"role": "user", "content": "写一篇2000字的技术文章"}],
    stream=True
)

full_content = ""
for chunk in response:
    # 每个chunk只包含一小段文本
    if chunk.choices and chunk.choices[0].delta.content:
        content = chunk.choices[0].delta.content
        full_content += content
        print(content, end="", flush=True)  # 实时打印

6.2 实际应用场景

流式输出在以下场景中特别有用:

  1. 网页聊天界面:用户输入问题后,立刻看到模型在"思考",而不是干等
  2. 长文档生成:不需要等几千字全部生成完
  3. 代码补全:IDE 里的 AI 助手,边写边提示

6.3 带 Reasoning 的流式输出

如果你用的是支持思考模式的模型(如 DeepSeek R1),流式输出还会包含 reasoning_content(思考过程)和 content(最终答案):

response = client.chat.completions.create(
    model="deepseek-v4-pro",
    messages=[{"role": "user", "content": "如何解决Java中的并发问题?"}],
    stream=True,
    reasoning_effort="high",
    extra_body={"thinking": {"type": "enabled"}}
)

reasoning_content = ""
final_content = ""

for chunk in response:
    if not chunk.choices:
        continue
    delta = chunk.choices[0].delta
    
    # 思考过程(如果模型支持)
    if delta.reasoning_content is not None:
        reasoning_content += delta.reasoning_content
    
    # 最终答案
    if delta.content is not None:
        final_content += delta.content
        print(delta.content, end="", flush=True)

七、Few-shot Prompting(少样本提示)

7.1 什么是 Few-shot

Few-shot 就是给模型看几个例子,让它学会你的格式或逻辑。就像教小孩做题:"你看,3+2=5,那4+1等于几?"

7.2 经典示例:情感分类

response = client.chat.completions.create(
    model="deepseek-v4-pro",
    messages=[
        {"role": "system", "content": "你是一个文本情感分类器,请将用户输入分为正面、负面或中性。"},
        
        # 示例1
        {"role": "user", "content": "这个产品真好用!"},
        {"role": "assistant", "content": "正面"},
        
        # 示例2
        {"role": "user", "content": "物流太慢了,等了两周。"},
        {"role": "assistant", "content": "负面"},
        
        # 示例3
        {"role": "user", "content": "商品收到了。"},
        {"role": "assistant", "content": "中性"},
        
        # 真正要分类的内容
        {"role": "user", "content": "客服态度特别好,解决问题很快!"},
    ],
    temperature=0.1,  # 分类任务要确定性高
    max_tokens=10
)

print(response.choices[0].message.content)
# 输出: 正面

7.3 进阶示例:结构化输出

让模型输出 JSON 格式的数据,方便程序后续处理:

response = client.chat.completions.create(
    model="deepseek-v4-pro",
    messages=[
        {"role": "system", "content": """你是一个信息提取助手。请从用户输入中提取姓名、年龄、城市,并以JSON格式返回。
只返回JSON,不要其他内容。格式如下:
{"name": "姓名", "age": 年龄数字, "city": "城市"}"""},
        
        # 示例1
        {"role": "user", "content": "我叫张三,今年28岁,住在北京。"},
        {"role": "assistant", "content": "{\"name\": \"张三\", \"age\": 28, \"city\": \"北京\"}"},
        
        # 示例2
        {"role": "user", "content": "李四,35岁,上海。"},
        {"role": "assistant", "content": "{\"name\": \"李四\", \"age\": 35, \"city\": \"上海\"}"},
        
        # 真正要处理的
        {"role": "user", "content": "王芳,25岁,广州人。"},
    ],
    temperature=0.0,  # 结构化输出要零随机
    max_tokens=100
)

import json
result = json.loads(response.choices[0].message.content)
print(result)
# {'name': '王芳', 'age': 25, 'city': '广州'}

7.4 Few-shot 的关键技巧

技巧说明
示例要贴近真实场景给模型看的例子,风格和格式要和真实输入一致
2-5个示例最佳太少学不到规律,太多浪费token
temperature 设低分类/提取类任务用 0.0~0.1
system prompt + 示例配合system 定规则,示例给示范

八、实战:代码审查 Agent

把我们学到的所有知识整合起来,写一个真正的实用工具——代码审查 Agent

8.1 完整代码

import os
import json
from openai import OpenAI

if __name__ == "__main__":
    # 1. 加载配置
    with open("config_bigmodel.json", "r", encoding="utf-8") as f:
        config = json.load(f)

    client = OpenAI(
        api_key=config["deepseek"]["api_key"],
        base_url=config["deepseek"]["base_url"]
    )

    # 2. 读取待审查的代码
    code_path = os.path.join(
        os.path.dirname(os.path.dirname(os.path.dirname(__file__))),
        "oneWeek", "day1~2", "pythonchan.py"
    )
    with open(code_path, "r", encoding="utf-8") as f:
        code_content = f.read()

    # 3. 构建消息(System + User)
    messages = [
        {
            "role": "system",
            "content": """你是一个高级Python代码审查专家。你的职责是:
1. 找出潜在的Bug
2. 指出性能问题
3. 发现安全风险
4. 给出重构建议

请按照以下格式回答:
- 【Bug】问题描述 + 所在行 + 修复建议
- 【性能】问题描述 + 优化建议
- 【安全】问题描述 + 加固建议
- 【重构】代码改进建议

如果某类没有问题,可以不输出该类。"""
        },
        {
            "role": "user",
            "content": f"请审查以下Python代码:\n\n```\n{code_content}\n```"
        }
    ]

    # 4. 发送请求
    response = client.chat.completions.create(
        model="deepseek-v4-pro",
        messages=messages,
        temperature=0.2,    # 代码审查要严谨
        max_tokens=2048,    # 给足输出空间
        stream=False
    )

    # 5. 输出结果
    print("=" * 60)
    print("代码审查报告")
    print("=" * 60)
    print(response.choices[0].message.content)
    print(f"\n消耗tokens: {response.usage.total_tokens}")

8.2 运行效果

运行后会输出类似这样的审查报告:

============================================================
代码审查报告
============================================================
【Bug】问题描述:
1. 第41行:filename[0:2].isdigit() 在 filename 长度小于2时会报错
   所在行:41
   修复建议:改为 len(filename) >= 2 and filename[0:2].isdigit()

2. 第28行:finally 块中没有实际逻辑,是多余的
   所在行:28
   修复建议:删除 finally 块

【性能】问题描述:
1. 第39-48行:手动枚举构建新文件名,可以用 enumerate 简化
   优化建议:使用列表推导式

【安全】问题描述:
1. 第55行:os.rename 直接使用用户输入的路径,可能存在路径穿越风险
   加固建议:对 folder_path 做合法性校验

【重构】代码改进建议:
1. 将文件命名逻辑抽取为独立函数,提高可测试性
2. 使用 pathlib.Path 替代 os.path,代码更简洁
============================================================
消耗tokens: 1856

九、总结

今天我们一起学了:

知识点核心要点
API 调用所有大模型都遵循 OpenAI 兼容协议,换模型只需改 base_url
Request/Responsemessages 数组 + model 是核心,response 里有 token 消耗信息
System/User/Assistantsystem 定规矩,user 提问题,assistant 给答案;多轮对话要传历史消息
参数调优temperature 控随机性,top_p 控词范围,max_tokens 控长度
流式输出stream=True 逐字推送,适合聊天界面
Few-shot给模型看几个例子,让它学会你的格式

下一步

下期我们将深入 RAG 系统的进阶优化

  • 向量检索的多种策略(相似度、关键词、混合检索)
  • Chunking 策略怎么选(500 vs 800 vs 1000?)
  • Rerank 重排序:让检索结果更精准
  • 如何用 LangChain 简化 RAG 开发

本文代码基于 openai Python SDK,适配 DeepSeek、智谱 GLM 等国内大模型。

系列文章:Day 1-2:Python速学 + 环境搭建