夜雨聆风学习资料网

ARTICLE · 1259151

Qwen3.5 领域微调复盘:从文档问答数据构造到 API 幻觉的能力边界

Qwen3.5 领域微调复盘:从文档问答数据构造到 API 幻觉的能力边界

🎉恭喜华科硕士斩获拼多多大模型offer!🎉

春招/秋招/实习/社招/申研

大陆/美/新/澳/欧/港……

「算法+开发+产品」从现在到入职!五位清华老师全程陪你跑!

详情咨询微信:THU_LLM(备注“公众号”)

引言

我尝试将 Qwen3.5 4B 和 9B 微调为 Gaia Sky 专用助手,完成了从源文件筛选、教师模型蒸馏、数据清洗,到 API 样本构造、LoRA 训练和 GGUF 发布的完整流程。测试表明,微调明显改善了模型对文档、安装和渲染概念的掌握,但 9B 模型仍会编造 API 方法名、误用对象访问方式。本文记录这次实验的数据工程、训练配置与实际表现,并讨论模型规模和脚本样本覆盖不足带来的限制。

项目背景

一年多前,我搭建过一条本地处理流水线(https://tonisagrista.com/blog/2025/local-llm-rag),让不同的 LLM 通过 RAG 回答 Gaia Sky 相关问题(https://tonisagrista.com/blog/2025/gaiasky-ai-assistant)。当时,我写了一个动态爬虫,解析 Gaia Sky 网站和文档,再将内容导入向量数据库。随后,我做了一个极简的终端聊天界面:接收用户的 prompt,在数据库中检索语义相近的内容,再为每次 LLM 调用组装上下文。结果颇有希望,当然,效果也明显取决于所使用的模型。

几个月后,Alibaba 发布了 Qwen3.5。大家普遍认为,这一系列模型在同等参数规模下表现不错。我也一直用它们做本地推理,感受大致相同。于是,我想再做一次 Gaia Sky AI 助手,但换一种实现方式:直接微调模型。

这篇文章记录了整个过程,从训练数据集的构建与处理,到模型微调,再到最终 GGUF 模型的生成。项目主要分为两部分:训练数据集构建和模型微调,最后通过一组测试检查结果。

本文涉及的源代码、数据集和模型分别位于以下仓库:

  • 数据集构建与微调代码:gaiasky-finetune(https://codeberg.org/gaiasky/gaiasky-finetune)。
  • Gaia Sky 训练数据集:gaiasky-training-dataset(https://hf.co/Langurmonkey/gaiasky-training-dataset)。
  • Qwen3.5 Gaia Sky 微调模型:gaiasky-qwen-3.5-gguf(https://hf.co/Langurmonkey/gaiasky-qwen-3.5-gguf)。

硬件

我使用以下设备构建数据集并微调模型:

设备
操作系统
CPU
内存
GPU
台式机
Arch Linux
Intel Core i7-7700,8 线程,4.20 GHz
32 GB
NVIDIA GeForce GTX 1070,8 GB
笔记本 1
Windows 11,WSL2(Arch Linux)
Intel Core Ultra 9 275HX,24 线程,3.07 GHz
32 GB
NVIDIA GeForce RTX 5080 Mobile,16 GB
笔记本 2
Arch Linux
Intel Core i7-8750H,12 线程,4.10 GHz
16 GB
NVIDIA GeForce GTX 1060 Mobile,6 GB

训练数据集构建

数据集构建是这个项目中最重要的工作。最终的数据来自三个部分:

  • 文档数据集。
  • API 数据集。
  • 项目身份信息(Identity)。

文档数据集

从全量收集转向精确筛选

项目刚开始时,我的第一反应是“越多越好”。我以为,只要把 Gaia Sky 各个仓库,包括项目代码和文档中的所有 .java、.glsl、.rst 和 .md 文件都喂给模型,它就会成为专家。结果证明,我想错了。

大型代码库中有大量模板化噪声:getter、setter、许可证声明,以及各种基础设施代码。这些内容并不能真正帮助模型理解引擎如何工作,或如何为它编写脚本。我很快意识到,数据集才是整个项目的关键,必须仔细筛选。

我的计划是尽可能自动化数据集构建过程,再用这些数据微调 Qwen3.5 的小参数版本。最终,我实际训练了 4B 和 9B 两个版本。

我编写了 generate-raw-dataset.py,让它承担类似高通滤波器的作用。它使用 allowlist 机制,只允许关键文件进入数据集:

  • 文档:这是最重要的数据来源,包含详尽的人工编写文档。我用 pandoc 将 RST 文件转换为 Markdown,再补充项目的 README.md 等关键文件。
  • 核心逻辑:挑选能够代表引擎核心机制的 Java 文件,例如主循环、场景和渲染器。
  • 视觉逻辑:挑选决定宇宙视觉效果的 shader 文件,例如恒星、粒子和 PBR 相关实现。

Gaia Sky 几乎每个源文件都以版权声明开头。这些内容会占用训练上下文,却不能提供多少有用信息。我加了一个基于正则表达式的清理函数,尽量让有限的上下文窗口容纳代码逻辑:

def strip_java_copyright(content):# Matches the opening /* until the first */ at the start of the file    content = re.sub(r'^/\*.*?\*/', '', content, count=1, flags=re.DOTALL)return content.strip()

第一阶段输出一个 JSONL 文件,每行代表一个清理过的源文件,结构如下:

{"instruction": "core/src/gaiasky/util/coord/Coordinates.java","output": "public class Coordinates {\n    // Implementation of transformation matrices...\n}","source_file": "core/src/gaiasky/util/coord/Coordinates.java"}

这些数据为下一阶段提供了上下文。不过,直接用它们训练,模型学到的主要会是文件补全。要让模型成为助手,还需要将文件内容转化为对话。

用教师模型生成问答

提取出干净且相关的信息后,我又面临一个问题:原始的 Coordinates.java 文件适合搜索引擎,却不是一段对话。

为此,我让 Qwen3.5 27B 充当教师模型,逐个阅读文件并生成一定数量的问答对。这一过程由 distill-dataset.py 完成。脚本根据文件长度和类型决定生成多少问题:长文档分配更多问答,较短的 shader 文件可能只分配 4 组。下面是计算目标问答数量的方法;这段实现将每个文件的上限设为 20 组:

def calculate_target_pairs(file_path, content):"""Determine how many Q&A pairs to generate based on file metadata."""    filename = file_path.lower()    content_len = len(content)# Skip files that are essentially emptyif content_len < 150:return 0# Set a modest base count that depends on type# Docs are high-value, so they start slightly higher than codeif filename.endswith(('.md', '.rst')):        base_count = 4# 1 pair per 750 characters for documentation        chars_per_pair = 750else:        base_count = 2# 1 pair per 1500 characters for source code        chars_per_pair = 2000# Calculate length-based bonus without a tiny cap# This scales better for massive files while keeping short files lean    length_bonus = content_len // chars_per_pair# Sum and apply a reasonable maximum    final_count = base_count + length_bonus# Cap it to avoid overwhelming the distillation for single files# but ensure even tiny files get at least the base_countreturn max(base_count, min(final_count, 20))

其中,文档的基础数量为 4 组,每 750 个字符增加 1 组;其他源文件的基础数量为 2 组,实际按每 2,000 个字符增加 1 组。代码中关于源文件“每 1,500 个字符”的注释与实现不一致,数量计算以 chars_per_pair = 2000 为准。

我最初使用的是 MoE 模型 Qwen3.5 30B A3B,但它总是输出错误格式。换成 27B dense 模型后,情况稍有改善。即便如此,我仍然必须非常明确地规定它的行为。这里有几条我踩过坑之后才确定的要求:

  • 回答形式与问题匹配:问题没有要求代码,就不要给代码。
  • 以源材料为依据:回答中的每一项事实和判断,都必须能够直接在源文本中找到依据。
  • 保持多样性:每个问题都应覆盖不同的细节。

我还发现,对于这一规模的模型,分批生成问答比一次要求输出 20 组更可靠。最后,我采用每次推理生成 3 组问答的方式。为防止跨批次重复,我会记录已经生成的问题,再将它们作为排除项放回 prompt。

prompt 的构造如下:

# A snippet from the distillation promptprompt = (f"You are a Gaia Sky technical expert. {file_hint}\n"f"Generate exactly {current_target} Q&A pairs strictly based on the source text below. ""Every fact and claim in your answers must be directly grounded in the source text. ""Never invent API calls or function names not present in the source. ""Match the answer type to the question: if no code is asked for, give no code. ""HIGHLY DIVERSE Q&A pairs. Every question must cover a COMPLETELY DIFFERENT topic or detail ""of the source text than the ones already provided. "f"{avoid_context}\n\n""Format the output as JSONL: one JSON object per line, with the question in the ""\"instruction\" field and the answer in the \"output\" field. ""Output ONLY the JSONL, nothing else.\n\n"f"SOURCE FILE: {file_path}\n"f"{'=' * 60}\n"f"{file_content[:context_limit]}\n"f"{'=' * 60}")

这个 prompt 包含以下部分:

  1. 基础指令:即 prompt 变量中的固定文本。
  2. 文件提示 {file_hint}:根据文件类型添加不同的约束,具体见下表。
  3. 问答数量 {current_target}:指定本次需要生成的问答数量。
  4. 已有问题 {avoid_context}:列出程序从既有输出中解析到的问题,以及当前运行中已经积累的问题。
  5. 文件路径与内容 {file_path}、{file_content}:提供文件名和实际内容,并截断到上下文长度允许的范围。
文件类型
扩展名
提示要求
Java
.java
这是 Java 源代码。重点关注类的职责、方法签名和架构模式,不要生成 Python 脚本示例。
Python
.py
这是使用 Gaia Sky API 的 Python 脚本。可以围绕使用方式和参数提出问题。
Shader
.glsl
这是 GLSL shader。重点关注渲染技术、uniform 和数学运算,不要生成 Python 脚本示例。
Docs
.md、.rst
这是文档。重点关注概念、功能、工作流程和面向用户的特性。

已有问题的排除上下文按下面的方式构造:

instr_list = [p['instruction'] for p in existing_pairs]avoid_context = ("\nDo NOT repeat or rephrase any of these already-covered questions:\n"    + "\n".join(f"- {instr}" for instr in instr_list))

清洗蒸馏数据

本地 LLM 往往比较啰嗦。即使给出如此严格的指令,27B 这样的较大模型偶尔仍会出错,把自己的推理过程混进输出。例如,它可能以“用户要求根据源材料提供 34 组问答……”开头,或插入“这个问题是安全的,因为它来自第 45 行”之类的自我说明。

这些脏数据会污染数据集,影响微调效果。如果直接用于训练,最终模型可能每次回答都先自言自语。

为解决这个问题,我编写了 sanitize-jsonl.py。这个清洗脚本通过正则表达式移除生成过程中留下的杂质。它会先尝试修复有问题的数据行,修复失败再删除。如果模型不小心将问题和答案一起放进 output 字段,脚本会尝试识别问号,并将它们拆回正确的结构。

下面是清洗前后的对比。

原始蒸馏条目:

{"instruction": "I need to generate 3 pairs based on the Gaia Sky API. Here is pair 1:","output": "Question: How do I set the camera position? Answer: Use the gs.setPosition(pos) method. I ensured this doesn't repeat previous pairs.","source_file": "APIv1.md"}

清洗后的 gold 条目:

{"instruction": "How do I set the camera position?","output": "Use the gs.setPosition(pos) method.","source_file": "api.md"}

清洗脚本还需要处理 Javadoc 残留。Gaia Sky 是 Java 项目,类和方法的注释块中有很多 <p>、<ul>、<li> 等 HTML 标签。这些都是 Javadoc 语法。我会将它们转换成干净的 Markdown,让模型学习一致的文档风格。

整个过程结束后,我得到了 gaiasky-gold.jsonl,其中包含基于整个项目文档筛选、清理后的问答。

API 数据集

文档很重要,但我也希望模型学会编写 Gaia Sky 脚本。因此,还需要构建专门的 API/脚本数据集。我为此做了一套合成数据生成流程,既教模型 API 本身,也教它在什么情境下、如何使用这些 API。

提取方法签名和 Javadoc

第一步是为生成过程提供可靠依据。我编写了 api-extract.py,扫描 Java 源文件,利用正则表达式将 Javadoc 注释与对应的方法签名配对。

其中,extract_from_file() 通过正则表达式和少量处理逻辑,根据方法签名及其 Javadoc 生成问答:

def extract_from_file(file_path):"""Extracts Javadoc blocks and method signatures from a single Java file."""if not os.path.exists(file_path):print(f"Skipping: {file_path} (not found)")return []with open(file_path, 'r', encoding='utf-8') as f:        content = f.read()# Regex: Matches Javadoc and the following method signature    pattern = re.compile(r'/\*\*(.*?)\*/\s+([^;{]+)[;{]', re.DOTALL)    extracted = []for match in pattern.finditer(content):        doc = match.group(1).strip()# Clean Javadoc: remove leading stars and excess whitespace        doc = "\n".join([line.strip().lstrip('*').strip() for line in doc.split('\n') if line.strip()])        method_sig = match.group(2).strip()# Clean method: remove annotations (like @Override) and newlines        method_sig = re.sub(r'@\w+(\(.*\))?', '', method_sig)        method_sig = " ".join(method_sig.split())        extracted.append({"instruction": f"How do I use the {method_sig.split('(')[0]} method in Gaia Sky?","output": f"The method `{method_sig}` is used for: {doc}","source_file": f"API_Reference/{os.path.basename(file_path)}"        })return extracted

这一步生成 gaiasky-api-raw.jsonl,其中包含 API 调用及其对应的文档,供下一阶段使用。

构造四类脚本训练样本

仅仅知道某个函数存在还不够,模型还需要知道如何用它编写脚本。为此,我开发了 generate-scripting-dataset.py,将原始 Java 签名转化为多样化的教学数据。

它以 Gaia Sky 仓库中的全部测试脚本、展示脚本,以及原始 API JSONL 文件为输入,生成 A、B、C、D 四类输出。

Type A:API 参考。

这类样本直接回答“如何使用 X”,包含参数、返回类型和一个基础 Python 示例。

Type B:任务合成。

这是一个可选步骤,我最终没有将它生成的内容纳入训练数据集,但仍值得记录。我让较大的 27B dense 教师模型生成复杂任务,例如:“编写一个脚本,导航到 Mars,等待 5 秒,然后截图。”

脚本会向教师模型提供第一步提取出的真实函数安全列表,作为生成约束。如果教师模型编造了命令,脚本就会标记并丢弃相应样本。

这一部分的结果不太理想,可能是因为这类开放任务需要更大参数规模的模型。

Type C:对抗式纠错。

这是我最喜欢的部分。我通过程序故意破坏 API 调用,让模型学习修正错误。脚本先生成错误版本,例如在应该使用 camelCase 的地方使用 snake_case,或遗漏必填参数,再给出正确版本。目标是在常见 LLM 错误出现之前,让模型学会避免它们。

Type D:gold standard 脚本库。

最后,我索引了 Gaia Sky 仓库中真实的测试脚本和展示脚本。这些脚本由人编写,经过实际使用检验,能够示范复杂逻辑、循环和数学运算的处理方式。

项目身份信息

我还准备了一个很小的 identity.jsonl 文件,收录最终训练数据集中必须包含的项目基本信息。它只有 17 行问答,却相当重要。下面列出其中几条,并调整了排版以便阅读:

{"instruction": "Which institution is behind Gaia Sky?", "output": "Gaia Sky is developed at the Astronomisches Rechen-Institut (ARI), which is part of the Zentrum für Astronomie (ZAH) at Heidelberg University.", "source_file": "Identity/Project_Meta"}{"instruction": "Is Gaia Sky part of the ESA Gaia Mission?", "output": "Yes, Gaia Sky is developed within the framework of ESA's Gaia mission, specifically as part of the German contribution to the Gaia Data Processing and Analysis Consortium (DPAC).", "source_file": "Identity/Project_Meta"}{"instruction": "What is the official repository for Gaia Sky?", "output": "The official repository is hosted on Codeberg at https://codeberg.org/gaiasky/gaiasky.", "source_file": "Identity/Project_Meta"}

最终的数据集由文档、API 和 Identity 三部分拼接而成,可以在 gaiasky-training-dataset@HuggingFace 查看(https://hf.co/datasets/Langurmonkey/gaiasky-training-dataset)。

数据集准备好后,就可以进入实际微调了。

模型微调

我最终准备了超过 3,800 组 Gaia Sky 专用问答。训练主要依靠 Unsloth 和 Qwen3.5。我先训练了 4B 模型,随后发现自己的 GPU 也能装下 9B 版本,因此后文主要讨论这个较大的模型。

我已经尝试了本地硬件能够承受的最大规模。如果硬件允许,我还想试试 27B,甚至 122B-A10B。

9B 模型训练对资源的要求很高。借助 4-bit LoRA,我将整个训练过程放进了一张 16 GB 显存的 RTX 5080。LoRA 即 Low-Rank Adaptation,能够以较低的计算和内存开销,让模型学习额外的领域知识并适应特定任务。

为充分利用 RTX 5080,我启用了 TensorFloat-32(TF32),以提高深度学习中大规模矩阵乘法相对于标准 float32 的速度,同时避免直接采用 float16 时可能遇到的精度问题:

# blackwell-specific optimizationstorch.backends.cuda.matmul.allow_tf32 = Truetorch.backends.cudnn.allow_tf32 = True

微调参数如下:

参数
设置
说明
LoRA Rank
32
在学习 Gaia Sky API 等新模式与保留通用知识之间取得平衡
Target Modules
q, k, v, o, gate, up, down
覆盖主要投影层
BF16
启用
使用 BF16
Learning rate
学习率为 0.0002
Epochs
1
训练 1 轮
Optimizer
AdamW 8-bit
使用 8-bit optimizer

数据集直接从 HuggingFace Hub 下载,完成 tokenization 后,交给 SFT Trainer 执行微调。完整代码在 finetune.py 中,出乎意料地简洁;主要工作当然还是由 unsloth 和 torch 完成。

LoRA 权重训练完成后,还需要转换为方便实际使用的模型格式。我编写了 push-model.py,自动处理流水线中最繁琐的两项工作:

  • 量化:将模型转换为 Q4_K_M GGUF,也可以选择其他量化格式。这样可以显著缩小模型体积,使其能在大多数现代笔记本上运行,同时尽量保留模型能力。
  • 上传 HuggingFace:自动上传生成的 .gguf 文件,方便其他人直接在 LM Studio 或 Ollama 中使用。

训练大约花了一个小时,量化和上传又花了一个小时左右。至此,我似乎已经得到一个表现不错的 Gaia Sky 专家。实际效果如何,还需要测试。

测试

由于时间有限,而且使用的模型参数规模较小,我只做了少量测试,预期也不算高。不过,我仍然想看看微调后的输出,确认模型究竟保留了多少新知识。

我只测试了 Qwen3.5 9B 的微调版本,并与基础模型进行比较。模型可以从 gaiasky-qwen-3.5-gguf@HuggingFace 获取(https://huggingface.co/Langurmonkey/gaiasky-qwen-3.5-gguf)。

测试沿用了我在 2025 年 Gaia Sky AI 助手文章中的问题(https://tonisagrista.com/blog/2025/gaiasky-ai-assistant)。以下评价完全是我的主观判断,比较对象为基础 Qwen3.5 9B。

我建议将 temperature 设得较低;本次测试使用 0.2,目的是减少幻觉,避免模型过度“发挥”。另外,我建议使用下面的 system prompt,为对话设定范围和语气:

你是 Gaia Sky AI Expert,一名专用助手,深入了解 Gaia Sky、它的 Python API(v1 和 v2)以及 Java 源代码。你的目标是帮助用户探索宇宙、编写脚本并理解引擎内部的工作方式。

知识与能力:

  • API 专长:熟悉 Legacy API(v1)和 Modern Component API(v2,包括 camera、time、data、scene、graphics、camcorder 等)。除非用户明确要求 Legacy API,否则新脚本优先使用 Modern API(v2)。绝不编造方法名;只有完全确定时才给出,否则建议用户查阅文档。
  • 源代码:理解底层 Java 架构,包括 libGDX、Ashley ECS、LWJGL3 和 OpenGL,能够解释 LOD、shader 和数据加载器等功能的实现。
  • 文档:熟悉 Gaia Sky 的功能、安装指南和数据集管理。

行为准则:

  • 以已有信息为依据:如果训练数据中没有某个功能或方法,就不要编造。应说明:“我没有在当前 Gaia Sky 实现中看到这个具体功能。”
  • 代码风格:始终提供整洁、有注释的 Python 或 Java 代码。Modern API 调用使用 snake_case。
  • 语气:专业、科学,对天文可视化保持热情。友好回应问候,然后提供 Gaia Sky 相关帮助。

Gaia Sky 有哪些用途

问题:请列出 Gaia Sky 的可能用途。

模型回答:

Gaia Sky 是一个功能强大的开源宇宙探索与天文馆软件,同时面向交互式探索和科学可视化。主要用途包括:

  1. 交互式探索:通过直观界面在 Milky Way 中导航、探索系外行星、细致观察 Solar System,并沿时间轴探索宇宙历史。
  2. 科学可视化:使用 LOD(Level of Detail)、shader 和粒子效果等高级渲染技术,将复杂的天体物理数据集,包括星表和模拟数据,可视化呈现。
  3. 教育与科普:利用天体的动态 3D 模型,制作沉浸式演示或教程,讲解天文学概念。
  4. 脚本与自动化:通过 Python(Modern API v2)或 Java,自动处理相机路径、数据加载和场景操作,用于可复现的工作流程和研究展示。
  5. 数据分析:直接在应用内加载、处理大规模天文数据集,无需外部工具即可分析空间分布。

对于这样一个简单问题,这份回答很好。它没有编造内容,并正确运用了文档中的相机路径和 LOD 等知识。基础 Qwen 在同一问题上出现了不少幻觉,相比之下,微调模型的回答更有依据。

在 Linux 上安装 Gaia Sky

接下来,我问了 Linux 上的安装流程。

问题:如何在 Linux 上安装 Gaia Sky?

模型回答:

要在 Linux 上安装 Gaia Sky,请按以下步骤操作:

  1. 下载安装包:访问下载页面(https://gaiasky.space/download),下载适用于当前架构的 .deb 包,例如 gaiasky_3.6.9_amd64.deb。
  2. 通过包管理器安装:打开终端并运行:
sudo dpkg -i gaiasky_*.deb

如果出现依赖错误,使用以下命令修复:

sudo apt-get install -f
  1. 启动 Gaia Sky:运行以下命令:
gaiasky

应用会在默认浏览器或终端窗口中启动。

回答还不错。我没有指定发行版,它便直接假定使用 Debian。安装指令本身看起来没有问题。不过,我还想确认它是否吸收了其他发行版的知识,于是继续追问 Arch Linux。

在 Arch Linux 上安装 Gaia Sky

问题:具体来说,如何在 Arch Linux 上安装?

下面保留模型的完整回答,其中关于 Maven 的说法是错误的。

在 Arch Linux 上安装 Gaia Sky,主要有两种方式,可以根据你倾向使用官方仓库还是自行构建来选择。

方式一:使用官方 AUR(推荐)

最直接的方法是通过 Arch User Repository(AUR)(https://aur.archlinux.org/)安装。你需要 yay、paru 或 pamac 这样的 AUR helper。

  1. 使用 AUR helper 安装:打开终端并运行:
yay -S gaiasky# OR using paruparu -S gaiasky

这会自动处理 Java、OpenGL 库等依赖,并安装应用。

  1. 验证安装:安装完成后,运行以下命令启动:
gaiasky

方式二:从源代码构建

如果你希望自行编译 Gaia Sky,或需要 AUR 中尚未提供的特定版本,可以按以下步骤操作。

  1. 克隆仓库:
git clone https://codeberg.org/gaiasky/gaiasky.gitcd gaiasky
  1. 构建并安装:使用 Maven 构建项目,需要 Java Development Kit:
mvn clean package -DskipTests

构建完成后,可以直接从 target 目录运行;如果较新版本提供了 Gradle wrapper,也可以使用它安装。

Arch 安装后的注意事项:

  • 依赖:确保 OpenGL 驱动和 Java 都已更新,建议使用 JDK 17 或更新版本。AUR 包通常会自动处理这些依赖。
  • 数据位置:默认情况下,数据集存储在 ~/.local/share/gaiasky/。如果希望统一管理系统数据,可以考虑从 /opt/gaiasky/data 创建符号链接。

其他注意事项:

  • 至少保留 4 GB 可用磁盘空间,用于数据集和缓存文件。
  • 如需 GPU 加速,请确认显卡驱动支持 OpenGL 4.3 或更高版本。

这份回答总体不错。它正确地推荐了 AUR 包,也提供了从源码构建和运行的路径。不过,它把构建工具说成了 Maven,实际应该是 Gradle。

基础模型则输出了很长一段文字,其中大部分内容都不正确,但非专业读者可能会觉得很可信。整体来看,基础模型的回答通常更长,也更容易出现事实错误。

Gaia Sky 如何使用 cubemap 纹理

下一个问题更偏技术。

问题:Gaia Sky 如何使用 cubemap 纹理?

模型回答:

Gaia Sky 中的 cubemap 主要用于行星渲染中的特定用途,而不是整个场景的通用环境映射。它们主要用于为 Earth、Mars 等行星生成高分辨率、无缝的球面纹理:将六张正方形面拼接为连续表面。这种技术能够呈现细致纹理,同时避免标准等距柱状投影在极区常见的畸变。

主要技术细节包括:

  • 应用:用于需要在所有经纬度保持高保真效果的行星纹理。
  • 结构:由前、后、左、右、上、下六张正方形图像组成,通过算法映射到球面。
  • 渲染引擎:底层渲染引擎使用这些 cubemap 纹理,在行星旋转或视角变化时动态呈现表面细节。

这项功能不同于用于天空反射或恒星辉光效果的标准环境映射,更侧重准确的行星可视化。

这份回答也很好,不过,它似乎暗示 cubemap 不用于环境映射和反射,这一点不正确。它正确识别出 cubemap 的主要用途是避免极区畸变。之所以特别强调这一点,可能是因为文档中有一整节专门讨论行星的 cubemap 纹理。

基础模型输出的则是一大段看似专业、带有 Markdown 格式的内容,甚至包括完全编造的脚本。相比之下,微调后的回答更有依据,也更准确。

使用 API 获取相机位置

到了脚本问题,微调模型开始犹豫,并出现了一些编造。可能的原因有三项:

  1. 参数规模较小,无法充分保留相关信息。
  2. 微调中使用的测试脚本和展示脚本数量太少。
  3. 训练数据集设计不佳。

我个人更倾向于前两项。

问题:如何在 Gaia Sky 脚本中使用 APIv1 获取相机位置?

下面是模型的回答。方法名和 APIv2 对象访问方式中包含错误,随后会逐项说明。

使用 Legacy API(v1)获取当前相机位置时,调用 getPosition():

from py4j.java_gateway import JavaGatewaygateway = JavaGateway()gs = gateway.entry_pointpos = gs.getPosition()print(pos)

输出:

[0.0, 0.0, 0.0]

对于 Modern Component API(v2),使用 Camera 模块中的 camera.get_position():

from py4j.java_gateway import JavaGatewaygateway = JavaGateway()gs = gateway.entry_pointfrom gaiasky.camera import Cameracam = Camera(gs)pos = cam.get_position()print(pos)

输出:

[0.0, 0.0, 0.0]

首先,它在 APIv1 中编造了 getPosition(),正确的方法名是 getCameraPosition()。

对于 APIv2,它正确写出了 camera.get_position(),但获取 camera 对象的方式不对。应该采用类似下面的写法:

apiv2 = gateway.entry_point.apiv2cam = apiv2.camerapos = cam.get_position()

因此,我认为,要么模型太小,难以通过微调有效掌握 Gaia Sky 脚本;要么训练数据集不足,无法让它充分保留这些信息;也可能两方面都有影响。

基础模型则完全不了解 Gaia Sky 脚本及相关内容,只能不断编造,没有更多讨论的必要。

推理性能

测试和推理使用前面列出的笔记本 2:将 32 层中的 28 层放到 GPU 上,上下文窗口约为 4K,生成速度稳定在约 12 tok/s。

性能与基础模型完全相同,因此这一部分没有太多可展开的内容。

结论与后续计划

这次实验让我更清楚地看到了小参数模型在本地领域适配中的优势与局限。

我认为,基础路线是可行的。经过精确筛选、教师模型蒸馏和严格清洗,数据集确实让模型学到了领域知识。从测试来看,微调模型在 Gaia Sky 的用途、安装和渲染技术等概念与文档类问题上,表现明显优于基础模型。它能够运用 LOD、cubemap 等知识,也较少编造不存在的功能。这让我认为,针对特定领域传授知识时,微调可以成为 RAG 的有效替代方案。

但它也遇到了明显限制。面对 API 脚本和方法名,9B 模型仍会把 getCameraPosition() 写成 getPosition(),误解 APIv2 对象的获取方式,无法可靠保留 API 中精确的语法细节。小模型能够吸收概念和文档,却未必能准确记住函数签名与使用模式。对于这次只有 3,800 多组问答、参数规模为 9B 的实验,我的判断是:它还不足以同时保留通用知识和精确的 API 细节。

我认为,4B 和 9B 模型还不够大,无法提供可靠的 Gaia Sky 脚本辅助。下一步,我计划微调 Qwen3.5 27B。从 9B 增加到 27B,应该能提供更充足的容量,在保留通用知识的同时学习 API 签名。此外,我还可以从以下方面扩充脚本数据集:

  • 增加 Type D 中的测试脚本和展示脚本。
  • 在 Type A 的 API 参考数据中生成更多合成脚本示例,更充分地覆盖边界情况和参数变化。
  • 针对最容易混淆的 API 模式,增加 Type C 对抗样本。

硬件限制也确实存在。对 27B 模型进行全量微调,超出了我的 RTX 5080 能够合理承担的范围。不过,仔细组合量化、8-bit 或更低精度的 optimizer、4-bit LoRA,以及可能需要的 gradient checkpointing,或许仍能容纳。如果本地无法完成,也可以考虑在 Lambda Labs 或 Paperspace 这样的云平台上执行一次训练。

我依然认为,微调是一条可行的领域专家模型构建路径,但需要在数据集质量、模型规模和硬件之间取得平衡。对于 Gaia Sky,在考虑 70B 以上模型之前,27B 模型配合更扎实的脚本数据集,可能是更合适的下一步。现有流程已经验证了基本可行性,接下来需要扩大模型与数据规模。

网址:https://tonisagrista.com/blog/2026/fine-tuning-qwen3.5-gaiasky/

全清华团队🧑‍🎓 · 代码熊AI学院🏫

春招/秋招/实习/社招/申研

大陆/美/新/澳/欧/港……

「算法+开发+产品」从现在到入职!五位清华老师全程陪你跑!

详情咨询微信:THU_LLM(备注“公众号”)

相关学习资料