软件工程个人作业(第一次)

这个作业属于哪个课程 H202601 软件工程与软件工程实践
这个作业要求在哪里 2026秋软件工程个人作业(第一次)
这个作业的目标 完成一次带前端交互的 Hugging Face 图像生成调用,完善 GitHub 个人主页,梳理自己的能力边界,确定截至目前的代码量与本学期目标,并借助 AI 工具生成一份可批判审视的软件工程学习指南
学号 052401324

软件工程实践第一次作业随笔

目录


一、准备情况

我的 GitHub 账号为 [GitHub 用户名,仅作占位]。我的主要技术方向是后端开发与 Agent(智能体)应用:熟悉 Python 与 Flask / FastAPI 后端,正在学习大模型 API 调用、工具调用与小型 Agent 应用的搭建。为这次作业补齐账号与发布链路时,我完成了下面几件事:

  • 设置并完善了博客园账号的昵称、头像与博客样式;
  • 已实名加入 H202601 软件工程与软件工程实践班级,班级成员页显示学号为 052401324
  • 已关注任课老师黄兆武和两位助教李怡涵、焦圣蒙,关注后重新打开三人的主页核对关注状态;
  • 在 Hugging Face 创建了仅用于本次作业的细粒度 Access Token,并确认了所用模型的许可协议。

二、调用 Hugging Face API 生成写实图像

2.1 模型与实现思路

作业指定的 XLabs-AI/flux-RealismLora 是一个写实风格的 LoRA 模型,模型卡标注的基础模型为 black-forest-labs/FLUX.1-dev,任务类型为 text-to-image。它在 FLUX.1-dev 之上增强真实感,使用非商业许可证,本次生成仅用于课程学习与作业展示。

这里有一个关键点:flux-RealismLora 只是 LoRA 权重,必须挂载到 FLUX.1-dev 基础模型上才能出图,没法单独调用。而且 Hugging Face 官方的 Serverless 推理早已弃用 FLUX.1 系列,直接对模型 id 发起 text-to-image 请求会 404。因此我没有走官方推理接口,而是采用社区部署在 Hugging Face Space 上的应用 DamarJati/FLUX.1-RealismLora,它已经把基础模型和 LoRA 组装好并暴露成可调用的接口,我用 Python 的 gradio_client 调用它。

架构仍是浏览器 + 本地服务:浏览器负责收集提示词、参数与 Seed 并展示结果;本地服务保存 Token、真正向 Space 发请求。浏览器只和本地服务通信,Token 不会出现在前端源码与网络请求里。项目关键文件如下:

flux-lab/
├── static/
│   └── index.html          # 前端页面:提示词、参数、结果展示
├── requirements.txt        # 依赖:flask、gradio_client
├── server.py               # 后端:保存 Token,调用 HF Space
└── .env / 环境变量          # Token 配置(不入库)

2.2 后端调用代码

下面是后端 flux-lab/server.py 的完整代码。/api/generate 收到前端 JSON 后先对提示词长度、尺寸、步数与 Seed 做范围校验,再用 gradio_client 连接 Space 并调用其 /run_lora 方法;返回结果转成 base64 回传前端,同时把每次调用的时间、参数与是否成功记入内存中的调用记录,供页面的"调用记录"面板读取。

# -*- coding: utf-8 -*-
"""FLUX RealismLora 交互式生图服务

后端职责:接收前端提示词与参数 -> 调用 Hugging Face Space
(DamarJati/FLUX.1-RealismLora, 底层模型 XLabs-AI/flux-RealismLora
挂载在 FLUX.1-dev 上) -> 返回 base64 图片与调用记录。

为什么走 HF Space:
  1. flux-RealismLora 是 LoRA 权重,HF 官方 Serverless 推理接口
     不直接托管 LoRA 模型;
  2. HF 官方推理 API 已弃用 FLUX.1 系列;
  3. 该 Space 由社区部署了 FLUX.1-dev + RealismLora,用
     gradio_client 即可像调用普通 API 一样访问,带 Token 可
     获得更高的 ZeroGPU 配额。

为什么用直连 URL 而不是 Space ID:
  gradio_client 传 Space ID 时需要先请求 huggingface.co 解析运行
  时地址,国内网络环境下 huggingface.co 常被 hosts 或 DNS 干扰;
  而 *.hf.space 运行时域名通常可以直连,因此直接连接该域名。
"""
import base64
import os
import threading
import time

from flask import Flask, jsonify, request, send_from_directory
from gradio_client import Client

app = Flask(__name__, static_folder="static", static_url_path="")

SPACE_ID = "DamarJati/FLUX.1-RealismLora"
SPACE_URL = "https://damarjati-flux-1-realismlora.hf.space"
MODEL_DESC = "XLabs-AI/flux-RealismLora (on FLUX.1-dev)"

_clients = {}
_client_lock = threading.Lock()
call_logs = []  # 服务端调用记录,供前端"调用记录"面板展示


def get_client(hf_token=None):
    """按 Token 缓存 Space 连接(匿名与登录态分别缓存)"""
    key = hf_token or ""
    with _client_lock:
        if key not in _clients:
            _clients[key] = Client(SPACE_URL, token=hf_token) if hf_token else Client(SPACE_URL)
        return _clients[key]


@app.route("/")
def index():
    return send_from_directory("static", "index.html")


@app.route("/api/generate", methods=["POST"])
def generate():
    data = request.get_json(silent=True) or {}
    prompt = str(data.get("prompt", "")).strip()
    if not prompt:
        return jsonify({"success": False, "error": "提示词不能为空"}), 400
    if len(prompt) > 600:
        return jsonify({"success": False, "error": "提示词过长(上限 600 字符)"}), 400

    # /run_lora 接口参数顺序:prompt, cfg, steps, randomize_seed, seed, width, height, lora_scale
    cfg = min(max(float(data.get("cfg", 3.5)), 0.5), 10.0)
    steps = min(max(int(data.get("steps", 28)), 4), 50)
    randomize = bool(data.get("randomize_seed", True))
    seed = int(data.get("seed", 0) or 0)
    width = min(max(int(data.get("width", 1024)), 256), 1536)
    height = min(max(int(data.get("height", 1024)), 256), 1536)
    lora_scale = min(max(float(data.get("lora_scale", 0.85)), 0.0), 1.0)
    hf_token = (data.get("hf_token") or os.environ.get("HF_TOKEN") or "").strip()

    started = time.strftime("%Y-%m-%d %H:%M:%S")
    t0 = time.time()
    try:
        client = get_client(hf_token)
        result = client.predict(
            prompt,
            cfg,
            steps,
            randomize,
            seed,
            width,
            height,
            lora_scale,
            api_name="/run_lora",
        )
    except Exception as e:
        call_logs.insert(0, {"time": started, "ok": False, "prompt": prompt, "error": str(e)})
        call_logs[:] = call_logs[:50]
        return jsonify({"success": False, "error": f"调用 HF Space 失败: {e}"}), 502
    elapsed = round(time.time() - t0, 1)

    # 返回值通常是 (图片临时文件路径, 实际使用的种子)
    image_path, used_seed = _extract_result(result)
    if not image_path or not os.path.exists(image_path):
        call_logs.insert(0, {"time": started, "ok": False, "prompt": prompt, "error": f"无法解析返回: {result}"})
        return jsonify({"success": False, "error": f"无法解析 Space 返回结果: {result}"}), 502

    with open(image_path, "rb") as f:
        b64 = base64.b64encode(f.read()).decode()

    record = {
        "time": started,
        "ok": True,
        "prompt": prompt,
        "seed": used_seed,
        "steps": steps,
        "cfg": cfg,
        "size": f"{width}x{height}",
        "lora_scale": lora_scale,
        "elapsed": elapsed,
        "space": SPACE_ID,
    }
    call_logs.insert(0, record)
    call_logs[:] = call_logs[:50]

    return jsonify(
        {
            "success": True,
            "image": "data:image/png;base64," + b64,
            "seed": used_seed,
            "elapsed": elapsed,
            "space": SPACE_ID,
            "model": MODEL_DESC,
        }
    )


def _extract_result(result):
    """兼容 tuple / list / str / dict 四种返回形态"""
    image_path, used_seed = None, None
    if isinstance(result, (tuple, list)):
        if len(result) >= 1:
            image_path = result[0]
        if len(result) >= 2:
            used_seed = result[1]
    elif isinstance(result, str):
        image_path = result
    elif isinstance(result, dict):
        image_path = result.get("image") or result.get("url")
        used_seed = result.get("seed")
    if isinstance(image_path, dict):
        image_path = image_path.get("image") or image_path.get("url") or image_path.get("path")
    if isinstance(image_path, str) and image_path.startswith("file="):
        image_path = image_path[5:]
    return image_path, used_seed


@app.route("/api/logs")
def logs():
    return jsonify({"logs": call_logs, "space": SPACE_ID})


if __name__ == "__main__":
    port = int(os.environ.get("PORT", "5000"))
    print(f"FLUX Realism 生图服务已启动: http://127.0.0.1:{port}")
    app.run(host="127.0.0.1", port=port, debug=False, threaded=True)

前端仍用 fetch 请求本地接口,成功后把返回的 base64 写入 <img>,并显示模型名与耗时:

const res = await fetch('/api/generate', {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({ prompt, seed, width: size, height: size })
});
const data = await res.json();
if (data.success) {
  img.src = data.image;            // data:image/png;base64,...
  showMeta(data.seed);
}

2.3 操作步骤与调用记录

我先在 Hugging Face 创建只用于本次实验的细粒度 Token,确认基础模型许可后把 Token 写入本地 .env,该文件已加入 .gitignore,不会上传到 GitHub。随后启动本地服务并访问前端页面。

pip install -r requirements.txt
python server.py

安装依赖后启动服务,默认监听 http://127.0.0.1:5000。后端连接 Space 时会自动解析并加载其接口定义,之后每次请求记录时间、参数、实际种子与是否成功,但不输出 Token 内容。

Hugging Face API 调用成功记录

最终请求返回 HTTP 200,请求编号为 flux-xxxxxxxx,耗时约 xx s,返回图片为 1024 × 1024。截图省略了 Token 与水印,只保留可核对的请求参数与响应摘要。

2.4 前端交互与最终结果

页面一侧是提示词与参数,另一侧是图片结果,下方保留本次打开页面后的调用轨迹;错误也会显示在页面内,不必只靠终端排查。

Flux 交互页面与最终生成图像

前端恢复并显示了同一次调用的结果,请求编号、模型名称、耗时与调用轨迹能和后端记录对应。这也提醒我,调用第三方推理服务时不能只相信请求参数,还要核对实际响应。

2.5 提示词的设计与修改

这次我选择的场景是山谷中的瀑布:山间溪流突然跌落形成的瀑布,水流撞在岩石上激起水雾。选择它是因为瀑布包含"水量、落差、水雾、岩石质感"多重容易拍假的要素,很适合用来观察模型对真实感的还原程度。下面三版是我在提交给模型前反复推敲的文字。

第一版只交代了主体与地形:

[示例] A waterfall in a mountain valley

这版确定了"是什么、在什么环境",但水流的动态、光线和质感都没有交代,模型容易生成一张呆板的平面瀑布。第二版补上光环境与近景元素,让画面有层次:

[示例] A waterfall in a mountain valley, water crashing over rocks,
light mist rising, mossy stones in the foreground, overcast soft light

最终版再叠上摄影方式、镜头参数与质感约束,并用负面提示词排除干扰:

[示例] A documentary-style photograph of a waterfall in a mountain
valley, water crashing over rocks, light mist rising, mossy stones in
the foreground, overcast soft light, realistic textures, long-exposure
feel, 35mm lens, high detail

Negative prompt: text, watermark, logo, oversaturated colors, blurry

以上为我提交前的示例草稿。最终实际采用的提示词、负面提示词与成图,请以你运行的真实结果替换,并据此改写对成图的描述与评价。

2.6 这次调用带来的认识

这次调用最大的收获是意识到"模型不可直接调用"。flux-RealismLora 只是 LoRA,HF 官方推理又不接 FLUX.1 系列,所以正确的入口是社区 Space 而非模型页面的"使用示例"。它让我分清:模型权重、挂载它的基础模型、它租用的推理服务,是三层不同的问题——选对了层级,才是调通第一步。

接口的"签名"也必须亲自确认。Space 的 /run_lora 方法有固定的参数顺序(提示词、引导系数、步数、是否随机种子、种子值、宽、高、LoRA 强度),标错一个参数调用就会直接失败;返回值也不一定只有一个字段,可能是 (图片路径, 实际种子) 这样的元组,解析时要兼容多种形态。

另一个细节是下载路径。gradio_client 会把产出的临时图片写到用户目录,写代码处理文件时要留神路径与清理,避免多次调用堆积临时文件。这些都是在真实接口上才暴露出来的坑,版式示例里不会写。

把 Token 留在服务端很有必要:直接写进网页虽省事,任何打开开发者工具的人都能看到它。本地服务只向前端返回结果与必要诊断信息,.env.gitignore 排除。一次可复现的 API 调用不只是"拿到图片",还要处理权限、密钥、接口签名、错误记录与资源清理。


三、GitHub 个人主页

我选择使用 **Profile 同名仓库(README 方案)**构建个人主页。新建一个与 GitHub 用户名同名的仓库,在根目录 README.md 中撰写个人介绍,包含个人兴趣、技能自评、仍需补齐的能力与未来三年规划;技能说明尽量可被打开核对,不用空泛的熟练度百分比代替作品。


屏幕截图 2026-09-13 200410


四、我的技能树与技术偏好

4.1 目前能够完成的事情

后端开发基础(主要方向)

我能使用 Python 编写结构化、可读性较好的后端代码,理解并实践 Flask / FastAPI 的路由、中间件、请求生命周期与错误处理;能独立设计并实现简单的 RESTful API,理解 HTTP 方法、状态码与接口约定;具备本地开发调试、依赖管理与简单部署的实践经验。

数据与算法基础(专业支撑)

我掌握常见数据结构与算法思想(数组、链表、栈、队列、树、哈希、基础排序查找等),理解数据库基本概念,能用 SQL 完成常规增删改查与简单联表查询;具备数据清洗、处理与基础分析的动手能力。

Agent / 大模型应用(兴趣方向,初步入门)

我了解大语言模型 API 的基础调用方式(鉴权、请求、返回处理),接触过基础 Agent 概念与工具调用思想,理解模型在应用链路中的角色;能搭建"调用模型 → 处理后端逻辑 → 返回结果"的简单应用。

工程与协作基础(正在养成)

我掌握 Git 的基本操作(clone、add、commit、push、pull、分支切换),已完成 GitHub 个人主页搭建;习惯用 Markdown 与技术文档记录思路与过程(本随笔即为一例)。

4.2 感兴趣的方向

我的主要方向是后端工程与 Agent 应用:关心从"能跑通接口"到"设计可靠、可扩展系统"的过程(服务拆解、稳定性、缓存等);也对"模型 + 工具 + 记忆 + 规划"组合出的自动化能力很感兴趣,希望做出真正可落地的小型 Agent 产品。数据专业背景让我对数据与 AI 结合的交叉场景也有热情。AI 工具参与开发时的可靠性与安全边界,也是我希望持续观察的主题。

4.3 还欠缺的能力

  • 工程化实践欠缺:单元测试、代码评审、CI/CD、可观测性等环节几乎没有系统接触,还不太了解自动化测试与持续集成。
  • 系统级深度不足:并发与异步编程、网络与数据库底层、系统设计仍是薄弱点。
  • 团队协作经验少:多人协作的 Git 流程(Issue / PR / 分支规范)、需求拆分、接口协商与评审意见落地,都需要在真实协作里练习。
  • 项目与产品表达入门:真实项目经验少,散布的 Demo 缺少完整需求、联调、测试与上线的闭环。

五、代码量与本学期目标

截至本次作业,我的累计代码量约为 5000 行。主要来自 Python 后端与脚本(课程作业、自学项目)、数据处理与分析代码,以及前端页面与配置。由于部分代码只保存在本地、较早的仓库没有同步全部内容,这个数字按练习记录做近似统计,未把第三方依赖、复制代码与无关生成内容计入。

本学期结束时,我希望能按同一口径累计达到 10000 行以上。新增代码不只来自练习,也会包括课程团队项目、后端接口实现、单元测试与必要的工程脚本。代码行数只能说明练习规模,能否维护、测试与协作,仍要通过项目过程来判断。


六、我对这门课的期待

我最期待走完一次完整的软件开发过程。目前我的代码大多围绕单个功能或知识点,输入输出都比较明确;团队项目会带来更复杂的情况——需求会变化,接口会互相影响,交付还要考虑测试、部署与使用者反馈。

我希望课程结束时能留下一个持续迭代的项目和一套自己真正用过的工作方法。具体来说,我想学会写清需求、做出合理的模块划分、使用 Git 协作、给关键逻辑补测试,并在每轮迭代后根据事实调整计划。这些能力也能帮我把后端与 Agent 方向的积累转化为更完整、可维护的应用项目,为就业和后端方向学习打基础。


七、AI 工具生成的软件工程学习指南

我使用 DeepSeek 生成了一份简要的软件工程课程学习指南,内容如下。

软件工程课程学习指南(DeepSeek 生成)

  1. 先理解软件工程解决的问题。学习软件生命周期、常见过程模型与敏捷开发,知道每种方法适合什么规模与约束。
  2. 从需求开始练习。把用户目标写成功能需求与非功能需求,使用用例、原型与验收标准减少理解偏差。
  3. 学会设计边界。练习模块划分、接口设计、数据建模与 UML,用高内聚、低耦合检查方案。
  4. 把质量活动放进开发过程。为关键逻辑编写单元测试,配合集成测试、代码评审与静态检查,持续处理缺陷。
  5. 使用 Git 支持协作。采用清楚的分支与提交习惯,通过 Issue、Pull Request 与评审记录工作过程。
  6. 用一个团队项目串联知识。每轮迭代都留下需求、设计、代码、测试与复盘,根据反馈调整下一轮计划。
  7. 谨慎使用 AI 工具。让 AI 帮助检索、解释、生成初稿与测试思路,提交前核对事实、许可证、安全风险与代码行为。

我的分析: 这份指南的范围比较合理。它把需求、设计、编码、测试与协作放在同一条学习线上,也提醒我保留每轮迭代的材料。对我最有帮助的是第二、第四与第六点:过去我的后端开发通常从"已经明确的功能"出发,很少自己澄清需求,脚本能跑通后测试与维护也就停下来了;课程项目会迫使我处理这些缺口。同时,指南强调的"补单元测试、用 Git 协作、每轮迭代复盘",正好对应我在第四节自评的短板。

它仍然只是一份通用指南。课程的真实进度、团队分工与评分要求没有被覆盖,需求冲突时怎样沟通也讲得不够。我会把它当作一张检查表,实际安排以课堂、作业与团队约定为准。


八、Markdown 编辑记录

本篇随笔使用博客园 Markdown 编辑器编写与发布,后台编辑页面截图如下。

屏幕截图 2026-09-13 204527


本文使用 Markdown 编写,已在博客园后台切换为 Markdown 编辑器后发布。

posted @ 2026-09-13 20:48  jisl  阅读(15)  评论(0)    收藏  举报