项目背景
现在进入公众视野的LLM产品泛化严重,当你让它给你生成某些代码时,你需要不断地提示它,纠正它,引导它,初期它会和你闲聊,根本没法高效完成你的任务,当它终于生成你想要的代码时往往会消耗比较多的时间,针对这个问题,我就想能不能自研一款web agent能够使得生成的代码能够做到规范、极简、效率高的风格呢?所以有了这次项目尝试!
项目实施过程:
阅读之前需拉入仓库,对应代码文件阅读。
git pull https://github.com/flowerundersky/-.git
建立项目总体文件架构,设计哪些文件夹放哪些文件,每个文件里写哪些内容,在项目主体部分,预期做成七个层级(从下到上):配置层、模型层、提示词层、工作图流层、适配层、解析层、用户层。层与层之间有对应接口函数。这样做的好处就是极大提高了测试和定位效率,为代码的DEBUG提供了极大的便利。
ui层:
ui层是系统的前端页面,使用的开发工具是gradio。它的主要工作是把自然语言收集起来,把请求发给 Go 网关,把 Go 返回的结果整理成适合前端展示的状态。按“界面搭建”和“事件处理”分开,build_ui 专门负责页面结构、样式、按钮和状态; _handle_* 函数们只负责一次点击的业务流转,ui层输出的是json格式的post请求(这里需要标注一下,post是一种http方法,还有其他的方法),能够打到go层。
解析器层: 解析器层的工作是对用户在ui层输入的自然语言进行处理(NLP),把它处理成adapter层需要的promptValue。解析具体链路:用户输入自然语言并点击解析按钮-->调用_handle_parse函数获得传入NLP模型的request(先把提示词和内容转变成langchain的chat模板的message,再调用invoke({})函数把message(str格式)转变成能传入generate()的langchain的PromptValue,generate()再把PromptValue做一次标准化转换成message(这一部是为了和tokenizer的模板格式对齐),再通过tokenizer的模板渲染成模型能够理解的格式inputs,最后再tokenize成pytorch张量喂给model.generate,这个是huggingface Transformers里causal LM的标准生成接口),得到的就是经过模型抽取好的字段,最后字段校验成parserrequestpayload的实例。
在这一层级得到了两个核心参量:trace(用来衡量自然语言处理层级的抽取和理解的好坏);request(用来给生成链路一个初始的用户请求)
adapter层: 将解析器层级输出的parserrequestpayload转换成GraphrequestPayload,最后再通过to_graph_request()变成真正给图工作流用的Graphrequest。
graph层:
这一层是整个agnet的核心,开发的重点就在这一层级上,可以按照业务完成任意形式的工作流,只要能画出流程图就能使用langgraph完成agent开发。在本项目中,graph层将adapter层输出的graphrequuest变成graphstate。然后开始走图工作流,首先判断如果请求里同时有 self_check、draft_code 和 review_result,就进入 revision;否则就表示普通生成,也就是传统langchain的链式生成。
状态流程图为:
START
-> build_prompt
-> generate
-> process
-> 如果 phase 是 review,回到 build_prompt
-> 否则 finalize
-> END
详细流程:
START 之后先走 _route_prompt,它只分出 “code” 和 “self_check” 两个入口名,但两条边都进同一个 build_prompt 节点。自检开启后,第一次运行通常还是先生成草稿。_process_generation_node 在 phase 为 generate 且 self_check=True 时,不直接结束,而是把模型输出保存成 draft_code,再把 phase 改成 review,见 graph.py:340。这时 _route_after_process 会把流程重新送回 build_prompt,于是进入自检审查模板,而不是继续生成代码。
审查模板明确要求模型只输出四个字段:review_result、next_step、recommend_continue、notes,见 prompt.py:164。模型返回后,_parse_review_outcome 会按冒号解析这些字段,并把 recommend_continue 归一成布尔值,见 graph.py:456。随后 _format_self_check_output 会把审查结果整理成可展示文本,里面会同时给出“自检结果”“下一步建议”“是否建议继续”和“当前草稿”。
当点击修订时,外部显式调用 continue_self_check_session,进入下一轮修订,start_self_check_session 和 continue_self_check_session 都会检查 review 是否真的产出,并且会把轮次限制在 max_rounds 之内。图本身不会自己无限循环。build_graph 里的回路只覆盖“生成后进入审查,再把审查结果回到最终输出”这一轮。
prompt层: 在提示词层级,其主要工作就是给模型写提示词,让他知道现在“system”端是什么样的,当前“human”端的要求是什么。这一层的输出的是ChatPromptTemplate对象,后续需要通过invoke({}),把实例化messages渲染为promptvalue. 在提示词层级,层级的任务是给项目需要生成的内容添加限定条件,这一层没有所谓的输入和输出,它只是自己在编写规则和要求,在chain层会去调用这个层级的函数或者规则。
配置层: config文件主要放生成模型的参数,项目的生成模型和自然语言处理模型都是(Qwen2.5-3B)。
DEFAULT_MAX_NEW_TOKENS
DEFAULT_MIN_NEW_TOKENS #控制模型最多(最少)生成多少个新字符 / 词
DEFAULT_TEMPERATURE #控制生成的随机性 / 创造性,范围:0.0-2.0
DEFAULT_TOP_P #核采样,控制词汇选择范围,范围:0。0-1.0,模型按概率选输出词
DEFAULT_REPETITION_PENALTY #重复惩罚,防止模型重复唠叨
do_sample #是否随机采样生成,范围:FALSE ,TRUE
模型层: 模型层级的主要工作是把prompt变成模型能吃的输入token,并把模型输出整理回文本。这个层级的具体工作主要是四个层面:第一层是抽象接口,定义了所有后端都要有的三个能力:加载、生成、关闭。第二层是模型生成参数整理,GenerationParams把外部传进来的配置统一成稳定结构,再转换成模型生成所需要的参数。第三层是具体后端实现。项目里目前只有QwenHFBackend,它是本地 Qwen 模型的 Transformers 实现。它的工作流程是:
- load():加载 tokenizer 和模型
- 如果 tokenizer 没有 pad token,就补一个
- 如果 tokenizer 没有 chat template,就装一个默认模板
- 根据 CPU / GPU 状态决定 dtype 和 device_map
- generate():把 prompt 转成输入,搬到设备上,调用模型生成,再解码回文本
- close():释放模型和 tokenizer,并清理显存
第四层是 prompt 适配和输出清理。这项工作非常重要,首先我们知道,当 src/graph.py 里执行 backend.generate(state["prompt"], ...) 时,传进来的通常是 prompt 模板渲染后的对象,不是纯字符串。generate() 会先把它标准化成 messages,再用 tokenizer 的 chat template 变成模型输入,最后调用 self.model.generate(...)。
其中有几个问题,第一,当langchain的prompt模板和模型的tokenizer模板不一致时,需要进行角色映射,在本项目中需要把 LangChain 的 human/ai 角色映射成 user/assistant。第二,当tokenizer没有chat template时,需要[_ensure_chat_template()]给没有 chat_template 的 tokenizer 补了一个兜底模板。第三,关于模型停止层面,模型自身在生成EOS之类的结束token或者到达最大token时,按照hugging face生成机制正常结束,当你发现生成内容里面有你不想要的,就要在外面再做一次字符串截断,就是说针对生成完的文本进行处理。
承接层: agent逻辑天天改,做业务智能体选用golang+python,agent逻辑不改,追求极致并发。本项目属于非商业用途,纯研究型,所以采用python+go的形式。
UI 按钮 -> 调 Go API -> Go 转发给 Python agent -> agent 执行工作流 -> 返回结果给 UI
cmd/server/main.go 只负责启动与组装,也就是所谓的 composition root。它做的是读环境变量、选 Python 可执行文件、初始化 bridge 和 HTTP server、处理优雅退出,不承载业务逻辑。通俗的来说就是负责“把整条链路接起来,规定监听哪个网址,规定用哪个python跑桥接代码。”
用户输入的是 message、self_check、revision_note ,经过处理,前端把输出的JSON智能体发给GO的HTTP接口(在httpapi)进行分流Go 的 HTTP 层先把请求体解成 requestPayload,再判断这次是解析预览、普通生成还是继续修正。最后把字段整理成桥接层请求,bridge 层再把结构化请求打包成一次 Python 调用,在桥接层里的invoke里,Go 会重新组一个 payload:
mode、message、self_check、revision_note、workflow_state把这个 JSON 通过标准输入喂给 Python 进程(go->agent的桥接进程)。python拿到JSON后,就进行对python agent的调用,先读取输入并加载JSON文件,然后根据mod分支选择
preview 只抽取结构化字段并返回 trace/request;
generate 会进一步跑 run_graph(...) 或 run_graph_with_state(...);
continue 会从 workflow_state 恢复上下文继续生成。
最后 Python 把结果 JSON化输出到 stdout。Go 收到 Python 的 stdout,再返回给前端 bridge 层把 Python 的 stdout 反序列化成 Response,HTTP 层再把它包成统一响应结构返回给前端。
项目测试端
测试的原则主要是这四个规则:
- 纯逻辑函数直接测输入输出,比如 tests/test_model.py 里的截断函数。
- 重依赖模块用假对象替代,比如 tests/test_parser.py 里用 DummyBackend,避免真实模型加载。
- 数据转换层重点测“字段有没有被规范化”,比如 tests/test_adapter.py。
- 编排层重点测“流程有没有走对”,比如 tests/test_graph.py。
项目目前仍存在众多有待优化的地方,持续更新,敬请期待。