15|使用LLM生成代码和测试
下载APP
关闭
渠道合作
推荐作者
15|使用LLM生成代码和测试
徐昊 · 徐昊 · AI时代的软件工程
课程介绍
讲述:徐昊
时长03:48大小3.49M
你好,我是徐昊,今天我们来继续学习 AI 时代的软件工程。
通过前面的学习,我们了解了如何使用大语言模型(Large Language Model,LLM)辅助进行业务知识管理。接下来,我们继续学习使用 LLM 辅助软件交付的整体流程,以及其中涉及到的知识管理。
从今天这节课开始,我们将进入如何使用 LLM 辅助软件开发的环节。让我们从一个例子开始。
命令行参数解析
我们中的大多数人都不得不时不时地解析一下命令行参数。如果我们没有一个方便的工具,那么我们就简单地处理一下传入 main 函数的字符串数组。有很多开源工具可以完成这个任务,但它们可能并不能完全满足我们的要求。所以我们再写一个吧。
传递给程序的参数由标志和值组成。标志应该是一个字符,前面有一个减号。每个标志都应该有零个或多个与之相关的值。例如:
-l -p 8080 -d /usr/logs
“l”(日志)没有相关的值,它是一个布尔标志,如果存在则为 true,不存在则为 false。“p”(端口)有一个整数值,“d”(目录)有一个字符串值。标志后面如果存在多个值,则该标志表示一个列表:
-g this is a list -d 1 2 -3 5
“g” 表示一个字符串列表[“this”, “is”, “a”, “list”],"d"标志表示一个整数列表[1, 2, -3, 5]。
如果参数中没有指定某个标志,那么解析器应该指定一个默认值。例如,false 代表布尔值,0 代表数字, "" 代表字符串,[]代表列表。如果给出的参数与模式不匹配,重要的是给出一个好的错误信息,准确地解释什么是错误的。
确保你的代码是可扩展的,即如何增加新的数值类型是直接和明显的。
首先,让我们直接将这段需求扔到 LLM 中,看看 LLM 会生成怎样的代码。当然在那之前我们需要将它转化为提示词模板:
需求
====
{requirement}
实现要求
====
使用 Java 语言实现上面的需求
当然你可以将 Java 语言换成任何你所熟悉的技术栈。我在这里仅仅是用 Java 举例。ChatGPT 给出的结果是这样的:
无论你是否了解 Java,你都可以感受到,这段代码的逻辑是有一定复杂度的。而 ChatGPT 生成这段代码也就只要几秒钟的时间,显然我们无法在几秒钟内判断这段代码是否符合我们的要求。
这就意味着,我们无法知道这段代码是否完成了功能。我们仍然要回归到为这段代码构造测试,并验证它是否正确的道路上。所以哪怕你对于测试驱动开发一无所知,LLM 的特性仍然会将你的注意力引导到测试上。我们可以在现在的代码基础上,让 LLM 帮助我们生成测试。
请根据需求,为这段代码生成相应的测试
ChatGPT 生成的结果是:
我们先忽略 ChatGPT 生成测试的风格问题,比如不同的场景应该构造成不同的测试,也可以暂时不考虑这些测试的完备性问题,比如缺少边界条件。先看看 ChatGPT 给出的代码是否能够满足这些测试。
第一次调试
然而结果是并不能,按照测试结果显示,当我们传入参数 “-l” 的时候,ChatGPT 生成的代码并没有按照题目要求的逻辑转化为 true。那么我们就要去回看 ChatGPT 生成的代码了,并尝试定位这个问题产生的根源。不难发现问题出在这个地方:
这里 ChatGPT 给出的逻辑是错误的,当我们给出参数 “-l” 的时候,实际的值是[“-l”],所以 values 是一个空列表,那么返回值应该是 true。我们可以按照这个向 ChatGPT 提出建议。
当然,我们也可以用一种更简单的方式,直接把错误信息提供给 ChatGPT,让它根据错误信息修改代码:
执行测试的错误信息如下,请根据错误信息,修改 CommandLineParser 的代码:
Expected :true
Actual :false
org.opentest4j.AssertionFailedError: expected:but was:
at org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:151)
at org.junit.jupiter.api.AssertionFailureBuilder.buildAndThrow(AssertionFailureBuilder.java:132)
at org.junit.jupiter.api.AssertEquals.failNotEqual(AssertEquals.java:197)
at org.junit.jupiter.api.AssertEquals.assertEquals(AssertEquals.java:182)
at org.junit.jupiter.api.AssertEquals.assertEquals(AssertEquals.java:177)
at org.junit.jupiter.api.Assertions.assertEquals(Assertions.java:1141)
at CommandLineParserTest.testParseArgs(CommandLineParserTest.java:15)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
再次执行测试,依然存在错误。
第二次调试
这次的问题要更严重一些,从错误结果上看,我们期待传入 “-p 8080” 时,得到参数的类型是整型,而 ChatGPT 生成的代码给出的结果是字符串。同样我们不难定位到错误在什么地方:
在这段代码中,ChatGPT 将任何的单值参数都当作了字符串类型。这背后反映的是 ChatGPT 给出的代码在设计上的缺陷。也就是说是,不同的参数 “-l”, “-p” 和 “-d” 对应了不同的类型(bool,int 和 string)。而 ChatGPT 给出的代码忽略了类型的差异,尝试从结果上去推断类型,这自然是不能成功的。那么如果我们仍然按照前面的做法,让 ChatGPT 自行解决,它会发现这个问题吗?
执行测试的错误信息如下,请根据错误信息,修改 CommandLineParser 的代码:
Expected :8080
Actual :8080
org.opentest4j.AssertionFailedError: expected: java.lang.Integer@1c5920df<8080> but was: java.lang.String@17f9d882<8080>
at org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:151)
at org.junit.jupiter.api.AssertionFailureBuilder.buildAndThrow(AssertionFailureBuilder.java:132)
at org.junit.jupiter.api.AssertEquals.failNotEqual(AssertEquals.java:197)
at org.junit.jupiter.api.AssertEquals.assertEquals(AssertEquals.java:182)
at org.junit.jupiter.api.AssertEquals.assertEquals(AssertEquals.java:177)
at org.junit.jupiter.api.Assertions.assertEquals(Assertions.java:1141)
at CommandLineParserTest.testParseArgs(CommandLineParserTest.java:16)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
很显然,它没发现这是个设计问题,但是它给出了一个能通过测试的最简单的办法。这种做法也蛮符合测试驱动开发对于节奏的要求。那么这时候,应该由我们给出设计上的建议和改进了。
需求中给出的参数具有不同类型,比如:
-l 是 bool
-p 是 int
-d 是 string
-g 是 list
我们需要指定不同参数的不同类型。然后根据类型去解析参数。请按照这个思路重新编写 CommandLineParser
提供建议以后,GPT 返回的修改结果如下。
第三次调试
按照 ChatGPT 给出的修改,我们仍然没能通过全部的测试。新的错误继续出现:
现在测试失败的地方在于,ChatGPT 生成的代码将 -3 看作了参数名,而不是参数值。现在我们可以直接告诉 ChatGPT 现在的问题在哪,问题出在下面这个方法中将参数类型硬编码了(hard-coded):
那么我们现在可以告诉 ChatGPT 这个问题,并让它重新生成代码:
目前 determineArgType 中限定了可以使用的参数,将它改编为可配置的情况。也就是当用户初始化 CommandLineParser 时,可以指定支持的参数及其类型。
GPT 的回复如下。
我们看到这里 ChatGPT 在 main 方法中给出了配置 CommandLineParser 的方式,我们需要将它改到我们的测试中。当然也可以用 ChatGPT 修改我们的测试,但其实没有这个必要。
小结
按照 ChatGPT 给出的修改,我们仍然没能通过全部的测试。新的错误继续出现:
这次是列表的结构错误。当然,这里我们可以继续告诉 ChatGPT,错误发生在哪,然后让 ChatGPT 再重写代码。过程与前面的类似,我们就不再重复了。
在这里我并不是想说 ChatGPT 生成的代码有多么的不靠谱。恰恰相反,哪怕漏洞百出,这仍然展示了巨大的效率提升。你可以试试自己从头开始编写这么一段功能,再比较一下这节课展示的方法,就会明白我的意思。
这里我想提醒你的是,LLM 快则快矣,质量堪忧。当我们使用 LLM 辅助软件开发的时候,更多的精力要放到质量的控制上。而不是一味地关注效率。
那么下节课,我们就来讲讲怎么在提高效率的同时保持质量。
思考题
请跟 LLM 一起完成课程中的这个题目。
欢迎你在留言区分享自己的思考或疑惑,我们会把精彩内容置顶供大家学习讨论。
1. 使用大语言模型(LLM)辅助进行业务知识管理,学习使用LLM辅助软件交付的整体流程,以及其中涉及到的知识管理。 2. 强调了即使使用LLM生成了代码,仍需要进行测试来验证代码的正确性,以及使用LLM生成测试的过程。 3. 发现使用LLM生成的代码存在问题,需要进行调试和修改,以及提出错误信息来指导LLM修改代码的过程。 4. 展示了调试过程中对生成的代码进行修改,以及再次执行测试仍存在错误的情况。 5. 强调了测试的重要性,以及在调试过程中不断修正代码和测试的过程。 6. 展示了对生成的代码进行修改后再次执行测试,依然存在错误的情况。
分享给需要的人,Ta购买本课程,你将得29元
生成海报并分享
2024-04-10
赞 3
提建议
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
上一篇
直播专场(二)|如何进行业务知识管理?
下一篇
16|任务划分与测试驱动AI开发
全部留言(5)
- 最新
- 精选
- 穿靴子的加菲猫2024-04-10 来自上海“l”(日志)没有相关的值,它是一个布尔标志,如果存在则为 true,不存在则为 false。“p”(端口)有一个整数值,“d”(目录)有一个字符串值。标志后面如果存在多个值,则该标志表示一个列表: -g this is a list -d 1 2 -3 5 “g” 表示一个字符串列表[“this”, “is”, “a”, “list”],"d"标志表示一个整数列表[1, 2, -3, 5]。 这里第二个d是不是换个字母好点,后文第二次调试时d代表字符串,会增加理解成本
作者回复: 原本需求就这样
- 术子米德2024-04-10 来自日本🤔☕️🤔☕️🤔 【Q】前面的课程,拉来认知模型,拉出LLM辅助建模和用户故事,做了这么多准备,怎么就跳跃到CleanCode的parseCmdLineArgs,这是出现幻觉了嘛?项目里,难道不是基于用户故事和建出来的模型,开始拆解到任务列表,让LLM浸泡在这个KnowledgeContext里持续辅助,然后用TDD的方法边验证、边实现、边详细设计、边螺旋迭代嘛? 忽然间切换KnowledgeContext,这个人也适用不了,更何况LLM怎么适应,这个跳跃真不懂。 — by 术子米德@2024年4月10日展开
编辑回复: 因为在你说这的这些之前,需要把技术知识处理掉。比如你说拆任务这个 那需要把架构和测试策略 放到llm里才行。后续课程里这个例子还会再做一遍,到时可以仔细对比一下两种方式的不同。
- 术子米德2024-04-10 来自日本//BEGIN UT CODE GENERATED BY GITHUB COPILOT, EXCEPT FIRST LINE COMMENT //use gtest to test the function CC_parseCmdLineArgs defined in CC_parseCmdLineArgs.h #include "CC_parseCmdLineArgs.h" #include <gtest/gtest.h> TEST(CC_parseCmdLineArgs, NullCmdLineArgs) { CC_CmdLineArgs_T CmdLineArgs; EXPECT_EQ(CC_FAIL, CC_parseCmdLineArgs(0, NULL, &CmdLineArgs)); } TEST(CC_parseCmdLineArgs, NullCmdLineArgsPtr) { EXPECT_EQ(CC_FAIL, CC_parseCmdLineArgs(0, NULL, NULL)); } TEST(CC_parseCmdLineArgs, NoArgs) { CC_CmdLineArgs_T CmdLineArgs; char *argv[] = { (char *)"test" }; EXPECT_EQ(CC_SUCCESS, CC_parseCmdLineArgs(1, argv, &CmdLineArgs)); EXPECT_FALSE(CmdLineArgs.IsLoggingEnabled); EXPECT_EQ(0, CmdLineArgs.RecvPort); EXPECT_EQ(NULL, CmdLineArgs.pLogSavingDir); } ... TEST(CC_parseCmdLineArgs, RecvPort) { CC_CmdLineArgs_T CmdLineArgs; char *argv[] = { (char *)"test", (char *)"-p", (char *)"1234" }; EXPECT_EQ(CC_SUCCESS, CC_parseCmdLineArgs(3, argv, &CmdLineArgs)); EXPECT_FALSE(CmdLineArgs.IsLoggingEnabled); EXPECT_EQ(1234, CmdLineArgs.RecvPort); EXPECT_EQ(NULL, CmdLineArgs.pLogSavingDir); } TEST(CC_parseCmdLineArgs, LogSavingDir) { CC_CmdLineArgs_T CmdLineArgs; char *argv[] = { (char *)"test", (char *)"-d", (char *)"/tmp" }; EXPECT_EQ(CC_SUCCESS, CC_parseCmdLineArgs(3, argv, &CmdLineArgs)); EXPECT_FALSE(CmdLineArgs.IsLoggingEnabled); EXPECT_EQ(0, CmdLineArgs.RecvPort); EXPECT_STREQ("/tmp", CmdLineArgs.pLogSavingDir); } TEST(CC_parseCmdLineArgs, AllArgs) { CC_CmdLineArgs_T CmdLineArgs; char *argv[] = { (char *)"test", (char *)"-l", (char *)"-p", (char *)"1234", (char *)"-d", (char *)"/tmp" }; EXPECT_EQ(CC_SUCCESS, CC_parseCmdLineArgs(7, argv, &CmdLineArgs)); EXPECT_TRUE(CmdLineArgs.IsLoggingEnabled); EXPECT_EQ(1234, CmdLineArgs.RecvPort); EXPECT_STREQ("/tmp", CmdLineArgs.pLogSavingDir); } int main(int argc, char **argv) { testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); }展开
- 术子米德2024-04-10 来自日本//BEGIN CODE&COMMENT BY ME typedef enum { CC_SUCCESS = 0, CC_FAIL = 1, } CC_Result_T; typedef struct { bool IsLoggingEnabled; //-l int RecvPort; //-p <port> char *pLogSavingDir; //-d <dir> } CC_CmdLineArgs_T; /** * @brief: Use CC_parseCmdLineArgs to parse command line arguments and save them in CC_CmdLingArgs_T. * * @param argc same as main * @param argv same as main * @param pCmdLineArgs pointer to CC_CmdLineArgs_T * @return CC_SUCCESS if successful, CC_FAIL otherwise in CC_Result_T */ CC_Result_T CC_parseCmdLineArgs(int argc, char *argv[], CC_CmdLineArgs_T *pCmdLineArgs);展开
- aoe2024-04-10 来自浙江使用 AI 生成的测试通过 9 个,失败 7 个。在已有代码上修补,比自己从 0 到 1 实现快了非常多,这个只用了十几分钟! 主要步骤: 1. 15 课的例子扔给 coze 的 GPT4,得到的代码简单粗暴 2. 使用 Optimize 功能优化一下 prompt 3. 在优化的 prompt 上添加自己的想法 4. 因返回 token 限制,需要多次对话完成结果 4.1 请给出 CommandLineParser 的完整代码 4.2 请给出完整测试代码 4.3 补全测试代码:测试用例可以继续增加,包括各种边界情况和无效输入 5. 最终得到的代码虽然测试没跑通,但功能强大展开共 1 条评论