LLM 工程化在福 uu 中的落地实践 —— 假期自动调课的智能解析

LLM 工程化在福 uu 中的落地实践 —— 假期自动调课的智能解析

技术向约 6.5 千字

—— 在对话框以外,大模型又能发挥什么作用呢?

在编写新版福 uu 后端代码的过程中,我们已经实现了从教务系统上读取调课信息并应用、展示到课程表上的能力。但这个调课的展示仅限于教师录入好的单次调课,对于教务处发布的全校性假日调课通知则并没有去做解析和展示的逻辑。

究其原因,是教务处发出的调课通知都长下面这个样子:

1、10 月 1 日(星期三)至 10 月 8 日(星期三)放假,共 8 天,全校本科生课程(含通识教育选修课)停课。

2、9 月 28 日(星期日)补上 10 月 7 日(星期二)的课,10 月 11 日(星期六)补上 10 月 8 号(星期三)的课,原 9 月 28 日和 10 月 11 日的课程停课。

这种自然语言格式的描述难以被代码解析。短短几行字背后,隐含着十余条调课规则 —— 哪些日期的课取消了?哪些日期的课被挪到了另一天?原来那一天的课又怎么办?

在过去,这些调课信息需要用户自行阅读通知、对照课表、在脑中完成推理,这极大地增加了用户的心智负担,同时也增加了同学们在补课的日子出现意外旷课的可能性。

但是 LLM 的出现,使这种任务有了可能的解决方案,我们可以利用其强大的自然语言理解能力来从中提取结构化的调课信息,从而直接给用户展示时间调整后的课程表,极大地提升用户体验。为了实现这个目标,我们需要对 LLM 进行工程化的封装,使其能够具备像传统解析函数一样的表征,从而优雅地接入到现有的业务流程中。

Why this?

在介绍我们的新方法以前,有必要先解释一下为什么这类通知不能用正则表达式或者规则引擎来解析。

首先是格式不固定。每次调课通知都可能由不同的老师起草,措辞、结构、标点习惯都各不相同。有时候会说「补上……的课」,有时候会说「调至……上课」,有时候会直接用「顺延」「提前」这样的表达。这导致了通知的格式不可能完全一致。

其次是语义嵌套复杂,单条规则背后往往隐藏着多个子规则。上面例子中的第二条,实际上包含了下面的几条逻辑:9 月 28 日原有课程停课;9 月 28 日按 10 月 7 日课表上课;10 月 11 日原有课程停课;10 月 11 日按 10 月 8 日课表上课。

如果用纯代码形式的规则引擎实现的话,会由于一种种边界情况的出现而越写越复杂,导致维护成本会随着边界情况的积累呈指数级增长。正因如此,LLM 的强大语言理解能力就能够派上用场了。

LLM as Function

很多人对大模型的第一印象就是一个对话界面 —— 你问它答。但在福 uu 的这个场景中,我们需要的是一种完全不同的方案,输入一段教务处的通知,在合适的提示词引导下让其能够准确地输出一组结构化的调课规则。

在传统的业务流程中,要从某段数据中解析出一些结构化的数据,几乎都是编写一个函数来封装好解析能力。对于调用方而言,函数完全是一个黑盒,只需要按照约定的方式传参、返回结果,至于内部实现究竟如何是不需要关心的。

既然这样的话,我们完全可以把 LLM 调用封装成一个函数来实现一些功能,我们称其为「LLM Function」。没有多轮对话,没有用户交互,甚至没有前端界面。大模型在这里扮演的角色,更像是一个和传统的正则表达式、规则引擎等处于同一个生态位的解析引擎。但和传统的解析引擎相对比,利用大模型解析可以充分地理解自然语言,泛化性更强,不需要囿于固定的、一定能被代码描述的格式。

这种函数仍然具有可测试、可替换的特性,并不会对后续的开发维护流程产生什么特别的影响。

具体的实现请参阅仓库 renbaoshuo/llmfunc

实现思路

把一次 LLM 调用看作一个普通函数 f(T) → R。

定义 Function[T, R] 接收类型为 T 的业务输入,返回类型为 R 的业务输出。调用者完全不需要关心中间经历了消息组装、模型推理、工具调用、响应解析这些细节,只需要 fn.Run(ctx, &input) 就能拿到强类型的结果。

核心是一个泛型结构体:

// Function defines an LLM-backed function.
//
//   - T is the input type (must implement FunctionInputFormatter interface).
//   - R is the output type.
type Function[T any, R any] struct {
    client *Client
    output OutputHandler[T, R]
    config *FunctionConfig
}

它会完成下面三件事:

  1. 将输入 T 转换为 LLM 的 ChatCompletionMessage 列表;
  2. 携带配置(Model、Temperature、Instruction 等)发起 API 请求;
  3. 将 LLM 的原始响应通过 output 函数转换为类型安全的 R

其中,T 是业务输入类型,相当于函数参数,R 是业务输出类型,相当于函数返回值。对于二者的具体处理,可以参见下面的介绍。

输入侧转换

框架在设计的时候没有强制要求输入必须是某种固定格式,而是通过 FunctionInputFormatter 接口做了一层解耦。

type FunctionInput struct {
    Messages []openai.ChatCompletionMessage `json:"messages"`
}

type FunctionInputFormatter interface {
    FunctionInput() *FunctionInput
}

任何业务类型 T,只要实现了 FunctionInput() 方法,就能产出一个 FunctionInput 告诉框架「如何把我自己转换成 LLM 能理解的消息列表」。这样每种业务场景可以自行决定 prompt 的构造方式,比如一个「信息提取」和一个「翻译请求」可以有完全不同的消息组装逻辑,但都能被同一个 Function 框架驱动。

为什么不直接让 T 就是 FunctionInput

因为那样会迫使所有业务逻辑直接面对 openai.ChatCompletionMessage 这种底层结构,业务语义就被淹没了。

之后在调用的时候,用实现好的接口生成构造向 LLM 发送请求使用的 FunctionInput 结构体。

if formatter, hasFormatter := any(*in).(FunctionInputFormatter); hasFormatter {
    input = formatter.FunctionInput()
    ok = true
}

这里的 any(*in) 是为了强制使用值类型实现 FunctionInput 的 formatter,避免值和指针混写的情况发生。

输出侧转换

对于 LLM 输出的解析,框架允许调用方在创建 Function 时自定义符合 OutputHandler[T, R] 解析器,也提供了 BypassOutputUnmarshalOutput 两种内置解析器。

  • BypassOutput() 直接返回一个包含 LLM 的原始输出字符串的结构体作为函数返回值;
  • UnmarshalOutput[T any, R any]() 则将输出反序列化为一个类型为 R 的结构体来作为函数返回值。

在创建 Function 时给定的输出解析器,会在每次调用函数的时候被用来解析 LLM 的输出。

Structured Output

一般情况下,UnmarshalOutput 会配合 Structured Output 一起使用。如果启用了 Structured Output(前提是模型支持),那么则会使用以下代码自动生成提供给模型的 Output Schema:

if config.structuredOutput {
    schema, err := jsonschema.GenerateSchemaForType(*new(R))
    if err != nil {
        panic(err)
    }
    OutputSchema(schema).Apply(config)
}

这段代码在 NewFunction 构造阶段执行,做了这几件事:

  1. new(R) 创建一个 R 类型的零值实例;
  2. jsonschema.GenerateSchemaForType 通过反射从这个零值中提取出完整的 JSON Schema;
  3. schema 存入 config

在生成 schema 的流程中,如果出现 error 只可能是 R 的类型定义本身有问题(比如包含了不可序列化的字段),这属于开发期就能暴露的 bug,所以直接 panic 是很合理的。

然后在 Run 阶段:

req.ResponseFormat = &openai.ChatCompletionResponseFormat{
    Type: openai.ChatCompletionResponseFormatTypeJSONSchema,
    JSONSchema: &openai.ChatCompletionResponseFormatJSONSchema{
        Name:        f.config.name,
        Description: f.config.description,
        Schema:      f.config.outputSchema,
        Strict:      true,
    },
}

把 schema 注入到 OpenAI API 的请求中,强制 LLM 按照这个 schema 输出 JSON。

这里值得展开说说 Structured Output 对于这类业务场景的重要性。在没有 Structured Output 的时候,常见的做法是在 Prompt 里写上「请以 JSON 格式输出」,然后在代码里用正则或者字符串处理去提取 JSON 块。这种方式很脆弱 —— 模型可能会在 JSON 前后加上解释性的文字,可能会使用 Markdown 代码块,也可能因为输出截断导致 JSON 不完整。有了 Structured Output,模型在解码阶段就被约束在 schema 定义的范围内,输出的合法性由模型服务本身来保证,客户端代码可以直接 Unmarshal 而无需任何前处理。

总结一下,这样设计的好处是,在整个过程中,你只需要定义一个 Go struct,框架自动保证 LLM 输出能被安全地反序列化回这个 struct。

注:如果模型支持 Tool Calling 但不支持 Structured Output 的话,可以通过劫持 instruction 强制要求模型调用 callback tool 的方式来实现结构化的输出,但这样子会对 Prompt 造成污染,因此还是尽量使用 Structured Output 为佳。

Functional Options

FunctionConfig 通过经典的 Functional Options 模式暴露配置项:

llmfunc.Name("auto_adjust_course")               // 函数名称
llmfunc.Description("解析调课通知")               // 函数描述
llmfunc.Instruction("你是一名调课通知解析助手...") // System Prompt
llmfunc.Model("deepseek-ai/DeepSeek-V3.2")       // 模型选择
llmfunc.Temperature(0.2)                         // 温度参数
llmfunc.StructuredOutput(true)                   // 启用结构化输出

每个选项都是一个实现了 Option[*FunctionConfig] 接口的值,通过 Apply 方法修改配置。这种模式的好处是 API 简洁、可扩展 —— 未来增加新的参数只需要加一个新函数,不需要修改已有代码。

在调课解析这个场景中,Temperature 设置为较低值(如 0.2)是一个有意为之的选择。调课通知的解析是一个事实提取任务,而非创意生成任务。我们希望模型的输出是稳定且确定的,而不是在多次调用之间产生差异。低 Temperature 使模型更倾向于选择概率最高的 token,从而让同一份通知的解析结果具有良好的幂等性。

业务接入

输入和输出类型

输入类型 AutoAdjustCourseInput 包含通知的标题和正文两个字段,并实现了 FunctionInputFormatter 接口,将二者拼接为一条 User Message:

type AutoAdjustCourseInput struct {
    Title   string `json:"title"   description:"通知标题"`
    Content string `json:"content" description:"通知内容"`
}

func (i AutoAdjustCourseInput) FunctionInput() *llmfunc.FunctionInput {
    return &llmfunc.FunctionInput{
        Messages: []openai.ChatCompletionMessage{
            {
                Role:    openai.ChatMessageRoleUser,
                Content: fmt.Sprintf("#%s\n\n%s", i.Title, i.Content),
            },
        },
    }
}

输出类型的设计刻意保持简洁。每条调课记录只需要两个字段:from_date 表示原本上课的日期;to_date 表示调整后的实际上课日期,若课程取消则留空。不需要额外的字段,取消和换课的语义完全由 to_date 是否为空来区分。这样子可以让模型不在多个字段之间做复杂的联动判断,让模型专注于主要任务,以提升其准确度。

type AutoAdjustCourseItem struct {
    FromDate string `json:"from_date" description:"调整前课程本应上课的日期,格式为 YYYY-MM-DD"`
    ToDate   string `json:"to_date"   description:"调整后的实际上课日期,格式为 YYYY-MM-DD,如果课程取消则留空"`
}

type AutoAdjustCourseOutput struct {
    Items []AutoAdjustCourseItem `json:"items"`
}

其中 description 会生成到 schema 中来帮助模型理解输出格式。具体支持的结构体标签可以查看 jsonschema 库中 reflectSchemaObject 的实现作为参考。

编写 System Prompt

System Prompt 的质量直接决定了解析效果的上限。特别是对于「A 日补上 B 日的课,原 A 日课程停课」类型的表述,它们实际上各自对应着两条独立的记录,需要在 Prompt 中将这个隐含的拆分逻辑显式地写出来,避免模型不知道怎么处理这种情况而产生错误。

我们实际使用的 System Prompt 是由两部分组成的:

  1. 逐条列举的解析规则;
  2. 一次完整的解析输入、思考、输出示例。
你是一名调课通知解析助手,负责从调课通知的标题和内容中提取调课信息。调课信息包括调整前的日期和调整后的日期(如果当日课程取消则留空)。
请根据以下要求提取信息:
1. 输入的通知中可能包含多条调课信息,请提取所有的调课信息。
2. 日期格式为 YYYY-MM-DD。
3. 对于通知中提到放假的日期,如果没有提到其对应的补课日期则表示课程取消,产生一条调课信息,调整前的日期是放假的日期,调整后的日期留空。
4. 如果通知中提到日期X补上日期Y的课,则原来应该在日期X上课的课程取消,产生两条调课信息:一条是日期X的课程取消(调整后的日期留空),另一条是日期Y的课程调整(调整前的日期是X,调整后的日期是Y)。
5. 输出的结果应该是一个包含多个调课信息的列表,每条调课信息包含调整前的日期和调整后的日期(如果当日课程取消则留空)。
6. 如果无法提取到有效的调课信息,请返回一个空列表。

【示例输入】
# 关于2026年元旦放假课程调整的通知

各学院,各教学单位:

根据党政办有关2026年元旦放假通知的精神,现将放假期间的课程调整如下:
1、1月1日(周四)至1月3日(周六)放假,共3天,全校本科生课程(含通识教育选修课)停课。
2、1月4日(周日)补上1月2日(周五)的课,原1月4日的课程停课。
3、因停课受影响的教学内容,请任课老师自行调整安排。

请各学院、教学单位及时通知相关师生。

教务处
2025年12月29日

【解析结果】
第一条信息提示1月1日到1月3日放假,停课,得到一个1月1日、1月2日、1月3日均取消的记录列表;
第二条信息提示1月4日补上1月2日的课,原1月4日的课程停课,得到一个1月4日取消的记录,将1月2日的取消记录改为调整到1月4日。
综上,得到如示例输出所示的结果。

【示例输出】
[
  {
    "from_date": "2026-01-01",
    "to_date": ""
  },
  {
    "from_date": "2026-01-02",
    "to_date": "2026-01-04"
  },
  {
    "from_date": "2026-01-03",
    "to_date": ""
  },
  {
    "from_date": "2026-01-04",
    "to_date": ""
  }
]

Prompt 中附带了一个完整的示例,以元旦放假通知为输入,逐步推导出最终的 items 列表。这种 few-shot 风格的引导,可以让模型在面对真实通知时直接按照以往的思路进行推理,极大地提升了推理过程的确定性。

组装 LLM Function

项目在 pkg/ai/function.go 中二次封装了一个薄薄的 NewFunction 函数,统一从配置中读取 API Key 和 Endpoint,避免在每个 LLM Function 里重复做一些获取配置的 dirty work。

func NewFunction[T any, R any](
    handler llmfunc.OutputHandler[T, R],
    opts ...llmfunc.Option[*llmfunc.FunctionConfig],
) *llmfunc.Function[T, R] {
    client := llmfunc.NewClient(config.AI.Key, config.AI.Endpoint)
    return llmfunc.NewFunction[T, R](client, handler, opts...)
}

基于这个构造函数,AutoAdjustCourse 函数的完整实现如下:

func AutoAdjustCourse(ctx context.Context, input AutoAdjustCourseInput) (*AutoAdjustCourseOutput, error) {
    f := NewFunction(
        llmfunc.UnmarshalOutput[AutoAdjustCourseInput, AutoAdjustCourseOutput](),
        llmfunc.Name("auto_adjust_course"),
        llmfunc.Description("解析调课通知提取调课信息"),
        llmfunc.Instruction(AutoAdjustCourseInstruction),
        llmfunc.StructuredOutput(true),
        llmfunc.Model("deepseek-ai/DeepSeek-V3.2"),
        llmfunc.Temperature(0.2),
    )

    output, err := f.Run(ctx, &input)
    if err != nil {
        return nil, fmt.Errorf("failed to run auto adjust course function: %w", err)
    }

    return output, nil
}

对于调用方而言,AutoAdjustCourse 和任何普通的解析函数没有任何区别,传入标题和正文,拿回结构化的 items 列表。多么优雅!

定时爬取通知

AutoAdjustCourse 被接入在通知同步的定时任务里。系统在 cmd/common/main.go 中配置了一个定期爬取教务处通知的定时任务 syncNoticeTask,每次抓取到新通知时,会异步地触发调课解析流程:

go func(notice *jwch.NoticeInfo) {
    ctx := context.Background()
    if err := commonSvc.NewCommonService(ctx, clientSet, taskQueue).ProcessAutoAdjustCourseNotice(notice); err != nil {
        logger.Errorf("ProcessAutoAdjustCourseNotice failed, title=%s url=%s err=%v", notice.Title, notice.URL, err)
    }
}(row)

ProcessAutoAdjustCourseNotice 在内部处理时,首先按标题过滤,跳过标题中不包含「课程调整」的通知,如果需要处理,则拉取通知详情页的完整正文,然后解析规则:

// 获取通知内容
detail, err := jwch.NewStudent().GetNoticeDetail(&jwch.NoticeDetailReq{
    WbTreeId: info.WbTreeId,
    WbNewsId: info.WbNewsId,
})

// 调用 LLM 从通知标题和正文中提取结构化的课程调整条目
result, err := ai.AutoAdjustCourse(s.ctx, ai.AutoAdjustCourseInput{
    Title:   info.Title,
    Content: content,
})

拿到 items 列表后,后续处理完全回归到确定性的代码逻辑 —— 将每条记录的日期字符串映射到所在学期,并计算出对应的周次和星期,最终构造出 model.AutoAdjustCourse 准备存入数据库:

// 根据 from_date 查找所属学期
term, found := utils.FindTermByDate(calendar.Terms, fromDate)

// 计算 from_date 在该学期中对应的周次和星期几
fromWeek, fromWeekday, err := utils.GetWeekdayByDate(term.StartDate, item.FromDate)

// 构造最终数据
adjustCourse := &model.AutoAdjustCourse{
    Year:        year,
    FromDate:    item.FromDate,
    ToDate:      toDate,
    Term:        term.Term,
    FromWeek:    int64(fromWeek),
    ToWeek:      toWeekPtr,
    FromWeekday: int64(fromWeekday),
    ToWeekday:   toWeekdayPtr,
    Enabled:     false, // 默认禁用,等待人工审核后再启用
}

所有记录写入完成后,统一刷新涉及学期的缓存,确保后续的课表查询能立即感知到新的调课记录。

应用调课规则

调课记录写入数据库并启用之后,在学生请求课表时,系统在拿到原始课程数据后,紧接着读取当前学期所有已启用的调课记录,并通过 getAdjustRules 生成匹配的调课规则,生成最终应用到课程上的 CourseAdjustRule 列表:

autoAdjustCourses, err := s.GetAutoAdjustCourseList(req.Term)

for _, c := range courses {
    adjustRules := getAdjustRules(c.ScheduleRules, autoAdjustCourses)
    c.ScheduleRules = jwch.ApplyAdjustRules(
        jwch.ApplyAdjustRules(c.ScheduleRules, c.AdjustRules),
        adjustRules,
    )
}

注意这里有两层 ApplyAdjustRules,第一层应用的是教师在教务系统中录入的单次调课(jwch 库解析出来的 c.AdjustRules),第二层才是我们新增的全校性假期调课(前面生成的 adjustRules)。

getAdjustRules 的核心逻辑是对于每条调课记录,遍历该课程的所有排课规则,找出「周次和星期与调课记录的 from_week / from_weekday 匹配」的那些排课,为其生成一条 CourseAdjustRule。这里就正好和前面的日期到周次和星期几的转换遥相呼应,通过预处理减少了在后续阶段的计算量。详细代码请参见 internal/course/service/get_course_list.go,这里就不再单独贴出来了。

多级缓存的设计

GetCourseList 的实现里,有一个特别的设计 —— 数据库和缓存存储的课表内容是不一样的。

// 写入数据库存储
originalCourses := pack.BuildCourse(courses)
s.taskQueue.Add(fmt.Sprintf("putCourse:%s", stuId), taskqueue.QueueTask{Execute: func() error {
    return s.putCourseToDatabase(stuId, req.Term, originalCourses)
}})

// ... 应用调课规则 ...

// 写入缓存存储
s.taskQueue.Add(courseKey, taskqueue.QueueTask{Execute: func() error {
    return cache.SetSliceCache(s.cache, s.ctx, courseKey, courses,
        constants.CourseTermsKeyExpire, "Course.SetCourseCache")
}})

数据库存储原始数据是为了保留重新计算的能力。调课规则是随时可能变化的 —— 管理员可能启用新规则,也可能修改或禁用已有规则。如果数据库里存的是某个时间点的处理结果,那么每次规则变更后都需要遍历所有用户的历史数据重新修改并写入,代价极高。而存储原始课表,规则变更后只需要让缓存失效,下次访问时自然会用最新的规则重新计算并回写缓存,做到了数据和规则的解耦。

缓存存储调课后的数据是为了让 hot path 足够快。对于活跃用户而言,每次打开 App 查看课表都应该是毫秒级响应。如果缓存里存的是原始数据,那么每次命中缓存后还要再走一遍调课信息的匹配逻辑,而调课信息通常是不会频繁变化的,导致白白增加计算开销。所以把已经处理好的结果直接放进缓存,命中时直接返回即可。

实际效果

以文章开头的国庆调课通知为例,来完整地走一遍我们设计的流程。

教务处通知发出后,syncNoticeTask 在下一个同步周期抓取到到新通知后,发现标题中含「课程调整」的关键词,随即在后台异步拉取通知正文并调用 AutoAdjustCourse 解析,解析后得到的结构化输出如下:

{
  "items": [
    { "from_date": "2025-10-01", "to_date": "" },
    { "from_date": "2025-10-02", "to_date": "" },
    { "from_date": "2025-10-03", "to_date": "" },
    { "from_date": "2025-10-04", "to_date": "" },
    { "from_date": "2025-10-05", "to_date": "" },
    { "from_date": "2025-10-06", "to_date": "" },
    { "from_date": "2025-10-07", "to_date": "2025-09-28" },
    { "from_date": "2025-10-08", "to_date": "2025-10-11" },
    { "from_date": "2025-09-28", "to_date": "" },
    { "from_date": "2025-10-11", "to_date": "" }
  ]
}
  • 10 月 1 日至 10 月 6 日连续停课;
  • 10 月 7 日的课调到了 9 月 28 日;
  • 10 月 8 日的课调到了 10 月 11 日;
  • 而 9 月 28 日和 10 月 11 日原有的课程则各自产生了一条取消记录。

这 10 条记录会先以未启用的状态存进数据库里,等待管理员审核后启用。启用后,所有查询该学期课表的请求都会经过 getAdjustRules 匹配,将受影响的那几节课从课表里移除,或者把它们调整到对应的补课日。用户打开 App,看到的已经是一张应用了全部调课规则的最终课表,不需要再对照通知自行推算。

整个过程对用户完全无感,LLM 解析发生在后台,与用户请求课表的链路没有任何交集;从用户视角来看,这和以往的课表体验没有任何不同,只是课表变「聪明」了。

后记

回顾整个流程,LLM 只做了它最擅长的那一件事 —— 自然语言理解,把教务处的通知翻译成结构化的课程调整信息列表。之后的日期到周次的换算、学期的归属判断、与每门课排课规则的匹配、缓存的刷新,全部由确定性的代码完成。两种技术各司其职,边界清晰。

这种架构在工程上带来了一个令人满意的性质:整个系统没有任何一个环节是只有 LLM 能做但却又无法被验证的黑盒。AI 层的输出可以被单元测试固定下来验证,业务层的每一条分支都可以通过 mock 独立覆盖,规则应用层是没有副作用的纯函数,数据库存储的是原始事实而非中间结论。把 LLM 当作函数来使用,意味着它必须接受和普通函数一样的工程约束 —— 可测试、可替换、职责单一 —— 而这些约束反过来也让整个系统的可靠性有了更坚实的保证。

这次实践中由 LLM 承担的部分,从代码量上看并不大,甚至可以说很小。但它完成的是在此之前我们一直没有好办法处理的格式不固定的自然语言通知的解析。这类通知隐含着复杂语义嵌套的调课逻辑,纯靠代码很难完成解析。过去这些信息只能靠用户自己去查询记录、专门留意,而现在这件事由我们的系统替用户完成了。

这次实践虽然由 LLM 实现的部分并不是很大,但它让我们看到了大模型并不只是聊天窗口里那个无所不知的助手,它同样可以是系统架构图上一个朴素而可靠的函数节点,能够和普通的代码有机结合。


相关代码:

本文所述业务接入部分的实现,是和正在参与西二在线 Go 组考核、福州大学 2025 级软件工程专业的吴佳伟同学一同完成的,在此表示感谢。感谢王智壹同学审阅本文并给出修改建议。

LLM 工程化在福 uu 中的落地实践 —— 假期自动调课的智能解析
本文作者
发布于
版权协议
转载或引用本文时请遵守许可协议,注明出处、不得用于商业用途!
喜欢这篇文章?为什么不考虑打赏一下作者呢?
爱发电