基于LangGraph与WorkBuddy构建AI编程学习型Skill:从代码生成到引导式教学
2026/8/26 22:56:36 网站建设 项目流程

1. 项目概述:从“贴Prompt”到“学技能”的转变

最近在AI编程社区里,一个现象越来越普遍:很多人拿到一个复杂的编程任务,第一反应不是去理解问题、设计算法,而是打开ChatGPT或者Claude,复制粘贴一段模糊的需求描述,然后期待AI直接吐出完美的代码。这被戏称为“贴Prompt工程师”。代码是跑起来了,但问题解决了吗?背后的逻辑懂了吗?下次遇到类似问题,能举一反三吗?恐怕很难。这种模式最大的弊端是,它剥夺了学习过程中最核心的“思考”和“构建”环节,让开发者变成了一个被动的代码接收者,而非主动的问题解决者。

我一直在想,有没有一种方式,能让AI编码工具不只是“代劳”,而是变成一个真正的“学习伙伴”?它应该引导你思考,帮你拆解问题,在你卡壳时给予提示而非直接给答案,最终让你自己构建出解决方案,并在这个过程中真正掌握知识和技能。这就是我尝试用WorkBuddy这个平台来构建一个“学习型Skill”的初衷。WorkBuddy本身是一个强大的AI代理(Agent)开发与运行平台,它允许你通过可视化的方式或代码(如LangGraph)来编排复杂的AI工作流。而“Skill”,在WorkBuddy的语境里,可以理解为一个封装好的、可复用的智能能力单元,它可以是数据分析、代码生成、文档解读等任何任务。

我这个Skill的目标很明确:将一次性的“贴Prompt求代码”行为,转变为一个结构化的、可交互的、旨在教会你某个编程概念或解决模式的“学习会话”。它不再是一个黑盒代码生成器,而是一个循循善诱的编程导师。接下来,我会详细拆解这个Skill的设计思路、核心实现、以及如何让它真正起到教学作用。

2. 核心设计思路:构建引导式学习工作流

要让AI编码变成学习,关键在于改变交互模式。传统的“输入-输出”模式必须被打破,取而代之的是一种“引导-思考-验证-迭代”的对话式学习循环。我的设计核心是围绕LangGraph来构建一个有状态的、多步骤的工作流。

2.1 从线性对话到有向图工作流

普通的聊天是线性的,你一句我一句,上下文容易丢失,目标也容易偏离。而LangGraph允许我们将一次学习会话建模成一个有向图(Graph)。图中的节点(Node)代表不同的“教学阶段”,边(Edge)则定义了根据当前状态(State)如何从一个阶段流转到下一个阶段。

对于这个学习型Skill,我设计了以下几个核心节点:

  1. 问题澄清与目标设定节点:当用户提出一个模糊的需求(如“帮我写个爬虫”),Skill不会立刻开始写代码。它会先反问一系列问题来明确边界:目标网站是什么?需要爬取哪些数据字段?是否有反爬机制需要考虑?预期的数据格式是什么?这个节点的目的是强迫用户先思考问题的全貌,而不是丢出一个模糊的指令。
  2. 概念拆解与方案设计节点:在明确目标后,Skill会引导用户将大问题拆解成几个关键的子任务或技术概念。例如,对于爬虫,可能会拆解为:网络请求(requests库)、HTML解析(BeautifulSoup或lxml)、数据存储(CSV/数据库)、异常处理与速率控制。对于每个子任务,Skill会简要解释其核心概念和常用工具,并询问用户是否理解,或者是否有更倾向的实现方式。
  3. 分步实现与代码共写节点:这是核心环节。Skill不会一次性给出完整代码。它会按照拆解后的子任务,一个接一个地引导用户实现。例如,它会先问:“我们先来解决网络请求部分,你知道怎么用requests.get获取网页内容吗?如果知道,你可以尝试写下这行代码;如果不知道,我可以先给你看一个最简单的例子。” 然后,根据用户的输入(可能是尝试的代码,也可能是“请展示例子”),Skill会给出针对性的反馈:修正错误、解释原理、或者展示一个基础示例并鼓励用户在其基础上修改。
  4. 代码审查与优化建议节点:当所有部分代码都“拼凑”出来后(无论是用户写的还是Skill辅助完成的),Skill会引导用户一起 Review 这段代码。它会从可读性、健壮性、效率等角度提出问题:“这里用try...except来处理网络超时是不是更好?”、“这个解析逻辑如果网页结构变了容易出错,有没有考虑用更稳定的选择器?” 这个过程旨在培养用户的代码质量和工程化思维。
  5. 总结归纳与知识延伸节点:最后,Skill会带领用户回顾整个解决过程,总结用到的关键库、核心函数和设计模式。并且,它会提出一两个相关的延伸问题或变种场景,鼓励用户尝试独立解决,从而巩固学习成果。

这个图状工作流确保了学习过程的结构化和目标导向,用户无法跳过思考直接拿到答案,必须参与每一个环节。

2.2 State的设计:记录学习旅程的上下文

LangGraph的State(状态)是贯穿整个工作流的数据容器。对于学习型Skill,State需要精心设计,以完整记录一次学习会话的上下文。我的State结构大致包含以下字段:

from typing import TypedDict, List, Annotated from langgraph.graph.message import add_messages import operator class State(TypedDict): # 对话消息历史,用于给LLM提供上下文 messages: Annotated[List, add_messages] # 用户原始问题 original_query: str # 经过澄清后明确的需求描述 clarified_requirements: str # 拆解出的子任务列表 sub_tasks: List[str] # 当前正在处理的子任务索引 current_task_index: int # 用户针对当前子任务已尝试的代码片段 user_attempt_code: str # Skill针对当前步骤提供的示例或指导代码 guidance_code: str # 最终整合的代码草案 draft_code: str # 记录用户在本会话中表现出的知识薄弱点(用于个性化) knowledge_gaps: List[str] # 会话是否已进入结束状态 is_finished: bool

通过这样一个丰富的State,工作流中的每个节点都能清楚地知道“我们现在在哪”、“我们已经做了什么”、“用户当前卡在什么地方”,从而做出最合适的教学决策。例如,“分步实现节点”会查看current_task_indexsub_tasks来决定当前要教什么,并参考user_attempt_code来决定是给予鼓励还是纠正错误。

2.3 教学策略与LLM提示词工程

有了工作流和状态,还需要一个“大脑”来在每个节点做出智能的教学决策。这就是大语言模型(LLM)的角色。但直接用一个通用指令调用LLM是远远不够的,必须为每个节点设计高度定制化的、角色明确的System Prompt

注意:这里的Prompt不再是用户直接粘贴的模糊需求,而是我作为Skill开发者精心设计的、用于指导AI如何扮演“导师”的剧本。这是“用AI构建学习工具”和“自己贴Prompt”的本质区别。

以“概念拆解与方案设计节点”的System Prompt为例:

你是一位经验丰富的软件工程师,现在担任编程导师。你的任务不是直接解决问题,而是引导学生自己找到解决方案。 当前学生的问题是:{clarified_requirements} 你的目标是: 1. 将这个问题拆解成3-5个关键的、循序渐进的子任务或技术模块。 2. 对每个子任务,用一两句话解释其核心目的和涉及的主要编程概念或库。 3. 你的输出必须严格遵循以下JSON格式: { "sub_tasks": [ {"name": "任务1名称", "concept": "涉及的核心概念解释"}, ... ], "next_question": "一个用于确认学生理解或引导选择的问题,例如:'以上拆解你觉得清晰吗?或者你对哪个部分特别感兴趣,想先从它开始?'" } 记住,拆解的目的是搭建学习脚手架,而不是吓退学生。确保子任务粒度适中,逻辑顺序合理。

这个Prompt明确了AI的角色(导师)、目标(引导拆解)、输出格式(结构化JSON),并强调了教学理念(搭建脚手架)。这样,LLM的输出就是稳定、可控、符合教学设计的,可以直接被后续节点解析和使用。

相比之下,“分步实现与代码共写节点”的Prompt会更侧重于交互和反馈:

你是编程教练,正在指导学生完成子任务:{current_sub_task}。 学生之前已经了解了相关概念:{task_concept}。 学生刚刚尝试了以下代码:{user_attempt_code}。 请按以下步骤工作: 1. **分析**:首先评估学生代码。如果为空,进入第2步;如果有代码,检查其语法和逻辑,找出亮点和主要问题。 2. **反馈**:给出具体、积极的反馈。先肯定任何正确的部分,然后指出最关键的一个改进点。 3. **引导**:不要直接给出完整正确的代码。提供一个最简化的、针对当前核心问题的代码片段作为“提示”,或者提出一个引导性问题,让学生补充关键行。 4. **输出格式**: { "feedback": "你的反馈文本", "hint_code": "可选的、简短的提示代码(最多5行)", "guiding_question": "一个引导学生下一步操作的问题" }

通过这种精细的Prompt设计,AI的行为被约束在“教练”的轨道上,避免了它一时兴起把答案全盘托出,保证了学习的互动性和渐进性。

3. 使用LangGraph实现核心工作流

设计思路清晰后,实现就变成了用LangGraph将上述节点和状态连接起来。这里我选择用Python和LangGraph库进行开发,因为它对状态管理和流程编排的支持非常直观。

3.1 定义状态与图结构

首先,我们需要定义前面提到的State类型,并初始化图。

from langgraph.graph import StateGraph, END from typing import TypedDict, List, Annotated from langgraph.graph.message import add_messages # 定义状态类型(同前文,此处略) class State(TypedDict): ... # 初始化工作流构建器 workflow = StateGraph(State)

3.2 实现各个节点函数

每个节点都是一个普通的Python函数,它接收当前State,调用LLM(使用设计好的Prompt),然后更新State并返回。

以“问题澄清节点”为例:

import json from langchain_openai import ChatOpenAI # 或其他LLM llm = ChatOpenAI(model="gpt-4-turbo-preview", temperature=0.2) # 温度调低,保持稳定性 def clarification_node(state: State): """节点1:问题澄清""" messages = state['messages'] # 构建一个强引导性的系统提示 system_prompt = f""" 用户提出了一个编程需求:{state['original_query']} 你作为导师,需要先帮他澄清需求,而不是急于解决。 请提出2-4个最关键的问题,以帮助明确项目范围、输入输出、技术约束等。 你的输出必须是纯JSON格式:{{"questions": ["问题1", "问题2", ...]}} """ # 调用LLM response = llm.invoke([{"role": "system", "content": system_prompt}] + messages) # 解析LLM的JSON输出 try: clarification = json.loads(response.content) questions = clarification.get("questions", []) except: questions = ["你能更具体地描述一下你想要实现的功能吗?"] # 更新状态:将澄清问题添加到消息历史,并设置一个标志等待用户回答 new_messages = messages + [{"role": "assistant", "content": f"为了更好地帮你,我需要先了解一些细节:\n" + "\n".join([f"{i+1}. {q}" for i, q in enumerate(questions)])}] # 注意:这里不会直接更新`clarified_requirements`,需要等用户在下轮对话中回答 new_state = { "messages": new_messages, "clarification_questions": questions, # 临时存储问题 # ... 其他状态保持不变或设为初始值 } return new_state

其他节点如design_node(方案设计)、coding_node(代码共写)也遵循类似模式,只是System Prompt和状态更新逻辑不同。

3.3 编排节点与条件流转

将所有节点添加到图中,并定义它们之间的流转边。LangGraph的强大之处在于支持条件边(Conditional Edge),可以根据State的内容决定下一步走哪里。

# 添加节点 workflow.add_node("clarify", clarification_node) workflow.add_node("design", design_node) workflow.add_node("code", coding_node) workflow.add_node("review", review_node) workflow.add_node("summarize", summarize_node) # 设置入口点 workflow.set_entry_point("clarify") # 添加边(包括条件边) from langgraph.graph import END def route_after_clarify(state: State): """在澄清后,判断用户是否已回答了所有澄清问题""" # 这里需要一个逻辑来判断对话历史中用户是否已回复 # 简化处理:如果消息数大于一定值,且最后一条是用户的,则认为已回答 messages = state['messages'] if len(messages) > 2 and messages[-1]['role'] == 'user': return "design" # 进入设计阶段 else: return "clarify" # 继续澄清(或进入一个等待用户输入的节点) def route_after_design(state: State): """设计完成后,进入第一个编码子任务""" if state.get('sub_tasks'): state['current_task_index'] = 0 # 初始化索引 return "code" else: return "design" # 设计未完成,返回 def route_after_code(state: State): """一个子任务编码后,判断是否还有下一个""" idx = state['current_task_index'] tasks = state['sub_tasks'] if idx < len(tasks) - 1: state['current_task_index'] = idx + 1 return "code" # 继续下一个编码任务 else: return "review" # 所有任务完成,进入审查 def route_after_review(state: State): """审查后,判断是否需要进行修改迭代""" if state.get('needs_revision', False): # 假设State中有一个需要修订的标志 state['current_task_index'] = state['revision_target'] # 设定要修订的子任务索引 return "code" else: return "summarize" # 添加条件边 workflow.add_conditional_edges( "clarify", route_after_clarify, {"clarify": "clarify", "design": "design"} # 可能的目标节点映射 ) workflow.add_edge("design", "code") # 设计完直接进入编码,由route_after_design控制具体跳转逻辑 workflow.add_conditional_edges("code", route_after_code, {"code": "code", "review": "review"}) workflow.add_conditional_edges("review", route_after_review, {"code": "code", "summarize": "summarize"}) workflow.add_edge("summarize", END) # 编译图 app = workflow.compile()

这样,一个具备基本智能导航能力的学习工作流就构建完成了。app可以被集成到WorkBuddy平台中,作为一个可调用的Skill。

3.4 在WorkBuddy中部署与集成

WorkBuddy平台通常提供多种集成方式。最简单的是通过其提供的SDK或API,将编译好的LangGraph应用包装成一个Web服务端点。

  1. 创建Skill项目:在WorkBuddy开发者界面,创建一个新的Skill,选择“自定义代码”或“LangGraph”类型。
  2. 封装Graph应用:编写一个简单的FastAPI或Flask应用,将app包装起来。这个应用接收用户输入,初始化State,调用app.stream()app.invoke()来推进工作流,并返回AI的响应。
    from fastapi import FastAPI from pydantic import BaseModel app_fastapi = FastAPI() class UserInput(BaseModel): message: str session_id: str | None = None # 用于支持多轮对话会话 @app_fastapi.post("/learn-to-code") async def learn_endpoint(input: UserInput): # 根据session_id获取或初始化State config = {"configurable": {"session_id": input.session_id or "default"}} # 将用户输入作为消息添加到状态 inputs = {"messages": [("user", input.message)]} # 流式或一次性调用图 for output in app.stream(inputs, config): # 处理输出,通常是最后一个节点更新的State pass # 从最终State中提取AI的最新回复返回给前端 final_message = output["messages"][-1].content return {"response": final_message}
  3. 配置Skill元信息:在WorkBuddy中配置Skill的名称、描述、输入输出参数(对应上面的UserInput模型)以及部署的端点URL。
  4. 测试与发布:在WorkBuddy的测试界面中,像真实用户一样与你的Skill交互,验证整个学习流程是否顺畅。调整各节点的Prompt和流转逻辑,直到体验满意,然后发布。

发布后,其他用户就可以在WorkBuddy的Skill市场或聊天界面中,像调用一个普通AI助手一样调用你这个“编程导师”Skill,但体验是完全不同的引导式学习。

4. 让学习真正发生的核心技巧与避坑指南

构建一个能运行的Skill只是第一步,如何让它真正有效教学,才是挑战。以下是我在开发和迭代过程中总结的核心技巧和踩过的坑。

4.1 技巧一:设计“恰到好处”的挑战梯度

这是最考验教学设计的部分。挑战太大,用户会沮丧放弃;挑战太小,用户学不到东西。我的策略是:

  • 基于State动态调整:在State中记录knowledge_gaps。如果发现用户在“异常处理”概念上反复出错,那么在后续涉及类似概念的环节,可以适当降低初始提示的难度,或者提供更详细的类比解释。
  • 提供“脚手架”与“撤脚手架”:初始阶段,提供较多的代码框架和提示(脚手架)。随着用户对某个子任务表现出理解(例如,能正确写出关键函数名),在下一个类似任务中,逐步减少提示(撤走脚手架),鼓励独立完成。
  • 多路径支持:在“代码共写节点”,准备多种响应模板。对于用户提交的空白或“我不知道”,给出一个基础示例;对于有尝试但出错的代码,进行针对性纠错;对于基本正确的代码,提出一个优化挑战。这需要LLM的Prompt具备很强的分支判断逻辑。

4.2 技巧二:强化反馈的“教学性”而非“评判性”

LLM很容易直接说“这里错了,应该这样写”。但这对于学习无益。必须训练它(通过Prompt)给出具有教学意义的反馈。

  • 错误归因:反馈应指向错误的原因,而不仅仅是现象。不说“你的循环索引越界了”,而说“因为列表data的长度是5,你的循环for i in range(1, 6):试图访问data[5],而有效索引是0到4。这通常是因为混淆了‘数量’和‘索引’的关系。”
  • 关联概念:将具体错误与之前讲解过的编程概念联系起来。“这里出现的KeyError,正好是我们之前讨论过的‘字典键不存在’的情况,还记得可以用.get()方法提供默认值来避免吗?”
  • 鼓励性语言:多用“很好的尝试”、“这个思路是对的,不过有个小细节”、“你已经抓住了核心,我们再完善一下……”这样的语言。积极反馈能极大提升学习动力。

4.3 技巧三:管理会话状态与长期记忆

一次学习可能无法掌握所有知识。一个理想的Skill应该能记住用户的历史交互。

  • WorkBuddy会话状态:利用WorkBuddy平台提供的会话管理,将session_id与LangGraph的configurable配置关联,确保同一用户多次对话能恢复状态。
  • 知识图谱化:在State中,不仅记录当前任务,还可以尝试用简单的结构记录用户暴露出的knowledge_gaps(如“不理解递归基”、“不熟悉列表推导式”)。虽然目前实现较复杂,但这是一个方向。当下,可以在总结节点,明确告诉用户:“本次会话中,我发现你在‘X’和‘Y’概念上可能需要更多练习,建议你下次可以尝试做一个涉及这些概念的小项目。”
  • 提供“存档”与“复盘”:在会话结束时,Skill可以生成一份简单的学习报告,包括解决的问题、用到的关键代码片段、以及核心概念总结,并鼓励用户保存。这给了用户实实在在的获得感。

4.4 常见问题与排查实录

在开发过程中,我遇到了不少典型问题,这里分享排查思路:

  1. 问题:LLM不遵循输出格式,导致节点解析失败。

    • 现象:在design_node,期望LLM输出JSON,但它却输出了一段自然文字。
    • 排查:首先检查System Prompt是否明确要求了JSON格式,并用json ...包裹示例。其次,检查LLM的temperature参数是否过高(建议低于0.3)。最后,在代码中必须添加健壮的异常处理(try...except),当解析失败时,可以回退到一个预定义的默认拆解方案,或者友好地提示用户“让我重新组织一下思路”,并重新调用LLM。
    • 解决:在Prompt中加入更严格的指令,如“你必须输出JSON,且只能是JSON,不要有任何其他解释文字。”同时,使用LangChain的StructuredOutputParser等工具可以强制格式,但可能会牺牲一些灵活性。
  2. 问题:工作流陷入死循环或在一个节点卡住。

    • 现象:用户和AI在“澄清需求”环节来回问答,始终无法进入下一阶段。
    • 排查:检查条件边函数route_after_clarify的逻辑。它判断“用户已回答”的条件可能过于简单或苛刻。例如,仅通过消息角色判断可能不准,因为用户可能回复“嗯”、“好的”这类无实质内容的语句。
    • 解决:优化状态流转逻辑。可以引入一个计数器或标志位。例如,在State中设置clarification_rounds,达到一定轮次后,即使问题未完全清晰,也强制进入下一阶段,并在设计节点补充说明“基于目前信息,我假设你的需求是...,我们先按此推进,过程中可以随时调整”。另一种方法是让LLM参与判断,在澄清节点最后,让LLM输出一个“是否已足够清晰”的布尔值作为状态的一部分。
  3. 问题:Skill反应慢,用户体验不流畅。

    • 现象:每个节点都需要调用LLM,导致多轮对话延迟明显。
    • 排查:LangGraph的流式响应(app.stream)可以边生成边返回,改善体验。但根本延迟在于LLM调用和复杂Prompt的处理。
    • 解决:a)优化Prompt:精简System Prompt,移除冗余指令。b)缓存:对于常见的、固定的教学回应(如某个概念的基础解释),可以不调用LLM,直接从本地缓存中返回。c)模型选择:在非核心推理环节,考虑使用更快、更便宜的模型(如GPT-3.5-Turbo)。d)异步处理:确保你的Web服务框架(如FastAPI)是异步的,避免阻塞。
  4. 问题:教学节奏单一,无法适应不同水平的用户。

    • 现象:新手觉得太快,有经验的开发者觉得太啰嗦。
    • 排查:初始设计时,Skill的教学节奏是固定的,缺乏个性化。
    • 解决:在会话开始时,增加一个“水平评估”环节。可以简单地问用户“请用1-5分自评你对Python的熟悉程度(1为新手,5为专家)”,或者通过第一个编码任务中用户的尝试代码来动态评估其水平。将评估结果存入State,后续节点的Prompt可以根据此水平动态调整解释的深度和代码提示的完整度。

5. 效果评估与未来迭代方向

构建并运行这个Skill一段时间后,我通过观察用户交互记录和收集反馈,来评估其效果。

积极效果

  • 用户参与度提升:与直接索要代码相比,用户在与Skill的交互中明显更活跃,提问更具体,尝试自己写代码的意愿更强。
  • 概念留存率提高:在后续的简单测试中,使用过该Skill学习某个概念(如装饰器)的用户,比仅阅读文档的用户,在解决变体问题时表现更好。
  • 正向反馈:用户普遍反映“感觉真的在学东西,而不是复制粘贴”、“卡住的时候给的提示正好是需要的,不是直接给答案”。

待改进点与迭代方向

  1. 个性化深度不足:目前的knowledge_gaps记录还很初步。未来可以探索与向量数据库结合,将每次会话中涉及的知识点、用户错误都嵌入存储,构建更精细的用户知识画像,从而实现跨会话的个性化学习路径推荐。
  2. 领域扩展:目前主要针对通用Python编程。可以创建垂直领域的Skill,如“Web开发学习Skill”、“数据分析学习Skill”,其中内置了该领域特有的任务拆解模式、最佳实践库和案例库。
  3. 从“学”到“练”:可以集成一个轻量级的代码运行环境(如基于Jupyter内核),允许用户在对话中直接运行、调试当前编写的代码片段,并获得实时错误反馈,形成“学-练-反馈”的闭环。
  4. 社区化与UGC:允许高级用户或教师,基于这个框架定制和发布自己的“教学图”(Teaching Graph),分享针对特定算法、框架的教学方法,形成一个可共享、可复用的教学Skill生态。

回过头看,这个过程本身就是一个绝佳的学习案例。我不再是“贴Prompt”去让AI帮我写一个Skill,而是深入理解了LangGraph的状态管理、工作流设计、提示词工程,以及如何将教育学的理念(如脚手架理论、最近发展区)融入AI应用。最终产出的不仅仅是一个工具,更是一套关于“如何用AI辅助深度学习”的方法论。这或许才是面对AI时代,我们开发者更应该聚焦的方向:不是让自己被替代,而是学会驾驭它,去创造能提升他人(包括自己)认知和能力的增强型工具。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询