很多人第一次接触 Function Call,都会有一种错觉:

这玩意不就是“让大模型调个工具”吗?

给模型塞几个工具描述,再让它返回一段 JSON,程序照着执行,不就完事了?

Demo 阶段,确实差不多。

但只要你真的把它往生产里推,很快就会发现,事情根本没这么简单。

因为 Function Call 一旦离开 PPT,真正要面对的问题就不再是“模型会不会调工具”,而是:

  • 它为什么总选错工具?
  • 参数为什么老是填错?
  • 为什么一个本来一步能做完的事,它要连续调 4 个工具?
  • 为什么工具明明返回错了,模型还能一本正经地胡说八道?
  • 为什么你以为自己做的是 Agent,最后其实只是“一个会随机调用接口的聊天机器人”?

说白了,Function Call 不是一个“锦上添花的小特性”,而是 AI 应用从“能聊天”走向“能做事”的那道坎。

跨过去了,才叫系统。

跨不过去,项目基本就会卡在“演示很好看,线上很难用”这个尴尬阶段。

今天不聊空概念,我们就聊点更真实的:

为什么很多 AI 项目看起来已经接上了 Function Call,最后还是做废了?

因为他们往往都死在下面这 5 步。


1. 第一步就理解错了:模型并没有“调用函数”

这是最常见、也最致命的误解。

很多人说:

Function Call 就是模型调用函数。

这句话听起来顺,实际上很容易把整个系统设计带歪。

更准确一点的说法应该是:

模型从来没有真正“执行函数”,它只是输出了一个结构化的动作建议。

比如模型返回:

{
  "tool_name": "search_docs",
  "arguments": {
    "query": "MCP 和 Function Call 的区别",
    "top_k": 5
  }
}

这说明了什么?

说明模型在说:

“我建议你现在去调一下 search_docs,参数大概这么填。”

注意,是“建议”。

真正干活的,不是模型,而是你的宿主系统。

这两者的区别为什么这么重要?

因为一旦你脑子里默认“模型能直接执行函数”,你就很容易漏掉下面这些关键环节:

  • 参数校验
  • 权限控制
  • 工具白名单
  • 超时重试
  • 结果清洗
  • 审计日志

最后就会出现一种很典型的系统幻觉:

你以为自己把执行能力交给了模型,实际上你只是把风险交给了线上。

所以 Function Call 最核心的一层,不是“调用”,而是结构化决策表达

模型负责出主意,系统负责拍板和执行。

这条边界不划清,后面全是坑。


2. 真正麻烦的,不是选工具,而是“参数终于出错了”

很多 AI Demo 看起来很丝滑,是因为它的工具参数极其简单。

比如:

  • 查一下天气
  • 搜一下文档
  • 查一下订单

参数通常就一两个字段,模型乱来也乱不到哪去。

但只要你把场景稍微做真实一点,问题马上就出来了。

比如一个企业内部 Agent,要支持:

  • 创建工单
  • 修改会议
  • 查询客户
  • 调 CRM
  • 调知识库
  • 调流程审批

这时候一个工具参数可能长成这样:

{
  "customer_id": "C1024",
  "priority": "high",
  "assignee": "sales_ops",
  "due_time": "2026-04-22 18:00:00",
  "notify_users": ["u1001", "u1002"],
  "tenant_id": "t_shanghai",
  "reason": "客户要求加急处理"
}

到了这里,模型就不再只是“会不会选工具”的问题了,而是:

  • 字段会不会漏
  • 枚举值会不会填错
  • 时间格式对不对
  • 多租户字段会不会串
  • 用户输入里没说清楚的字段,它会不会乱补

这也是为什么真正做过线上系统的人,很少会把注意力放在“模型选没选对工具”上。

他们更警惕的是:

模型给出了一个看起来很合理、实际上根本不能执行的参数集合。

这比完全不调用更麻烦。

因为它特别像真的,最容易骗过开发者。

所以成熟一点的系统,通常都会在参数层加一整套防线:

  • JSON Schema 校验
  • 类型校验
  • 必填字段校验
  • 枚举值约束
  • 默认值填充
  • 参数纠错或二次确认

如果业务更敏感,还会加:

  • 权限检查
  • 多租户隔离
  • 敏感字段脱敏
  • 人工确认节点

很多项目不是死在“模型不够聪明”,而是死在“大家太相信模型生成的参数”。


3. 能跑通一次,不代表它能稳定地“做完一件事”

这是第二个特别容易被忽略的点。

很多团队第一次做 Agent,都会很兴奋地展示:

“你看,它已经会调工具了!”

但问题是,会调一次工具能稳定完成任务,根本不是一回事。

一个真正可用的 Agent,往往不是一步结束,而是这样一个闭环:

理解问题
-> 判断信息是否足够
-> 选择工具
-> 生成参数
-> 执行工具
-> 读取结果
-> 判断下一步
-> 再次调用工具或结束

也就是说,真正难的地方不在第一跳,而在后面那几跳。

因为只要进入多步调用,系统就会立刻暴露出一堆新问题:

  • 模型会不会重复调用同一个工具
  • 它会不会在错误结果上继续推理
  • 它什么时候该停
  • 它会不会越调越偏
  • 它的步数、token、延迟还能不能控住

这就是为什么很多“看上去很聪明”的 Agent,到了线上体验特别差。

不是因为模型不会思考,而是因为系统没有给它设边界。

真正做生产的人,通常会给 Agent 加很多“刹车片”:

  • 最大调用步数
  • 单次任务 token 预算
  • 单工具调用频率限制
  • 超时终止
  • 重复调用检测
  • 异常分支回退

这些东西看起来一点都不性感,但它们往往才是 AI 系统能不能上线的关键。

说到底,Agent 的上限取决于模型,但下限取决于治理。


4. 大家都在聊工具调用,真正决定体验的却是“结果怎么喂回去”

这又是一个特别像细节、实际上特别核心的点。

很多人以为 Function Call 的主要工作在“前半段”:

  • 工具注册
  • 工具描述
  • 参数生成
  • 调用执行

但真正决定回答质量的,往往是“后半段”:

工具执行完之后,你到底把什么结果喂回给模型?

很多系统之所以看起来越做越乱,根本原因不是模型不会调工具,而是工具返回结果太脏。

常见问题包括:

  • 返回内容太长,直接把上下文塞爆
  • 下游接口字段太技术化,模型根本看不懂
  • 同一个工具返回了太多无关信息,模型被噪音带跑
  • 接口报错信息直接回传,模型拿错误文本硬编答案

举个很真实的例子。

你调了一个订单接口,原始返回长这样:

{
  "code": 0,
  "message": "success",
  "trace_id": "8f97c1...",
  "data": {
    "order_id": "A12345",
    "status": "shipped",
    "warehouse_code": "WH_09",
    "sync_version": 17,
    "operator_id": "sys_1029",
    "updated_at": "2026-04-22 10:30:00"
  }
}

这东西你直接喂回模型,模型未必能抓到重点。

更合理的做法通常是先整理成它更容易消费的结构,比如:

{
  "order_id": "A12345",
  "current_status": "已发货",
  "last_updated_at": "2026-04-22 10:30:00"
}

别小看这一步。

它本质上是在做一件非常重要的事:

把“系统返回结果”翻译成“模型可继续推理的证据”。

如果这层做不好,后面再好的模型也会被喂歪。

所以真正成熟的 Function Call 系统,通常都会单独做一层结果治理:

  • 结构化裁剪
  • 字段归一化
  • 错误语义标准化
  • 敏感信息脱敏
  • 长结果摘要
  • 关键证据提炼

很多时候,项目效果差,不是 Prompt 不行,而是喂回去的东西本来就是垃圾。


5. 最后一个坑,也是最大的坑:没有评测,你根本不知道自己做得到底好不好

这是最扎心的一点。

很多团队做 Function Call,到最后判断效果的方式非常朴素:

  • 跑几个样例
  • 看起来能用
  • 线上先试试

这在普通功能开发里都算危险,在 AI 系统里更是高风险操作。

因为 Function Call 最麻烦的地方就在于:

它不是“对”或者“错”这么简单。

它会出现大量灰度失败:

  • 工具选对了,但参数错了
  • 参数对了,但时机不对
  • 时机对了,但调了太多次
  • 调用成功了,但最终答案还是错的
  • 没调工具,结果模型幻觉反而显得更自然

如果没有评测体系,这些问题你几乎看不出来。

所以真正靠谱的团队,通常会盯这些指标:

  • 工具选择准确率
  • 参数生成正确率
  • 首次调用命中率
  • 平均调用步数
  • 平均 token 消耗
  • 平均响应时间
  • 最终任务成功率
  • 幻觉率

更进一步,还会专门构造测试集:

  • 明确应该调用工具的问题
  • 明确不应该调用工具的问题
  • 容易误选工具的相似问题
  • 参数容易填错的问题
  • 工具超时、报错、空返回的问题

你会发现,做到这里之后,Function Call 就已经完全不是“接个工具”这么简单了。

它开始越来越像一个完整的工程系统:

  • 前面是模型决策
  • 中间是协议和执行
  • 后面是结果治理和评测闭环

这也是为什么很多人觉得 AI 项目“落地难”。

不是难在模型本身,而是难在你要把一堆高不确定性的环节,硬生生做成一个稳定系统。


真正难的,从来不是让模型调一次工具

回头看你就会发现,Function Call 最容易骗人的地方就在这里:

它太容易做出一个“看起来已经成功”的 Demo 了。

模型能返回工具名。

参数也像模像样。

接口也调通了。

屏幕上甚至已经出现结果了。

于是很多人会下意识觉得:

这事差不多做完了。

恰恰相反。

大多数项目到了这里,其实才刚刚开始。

真正决定成败的,是后面这些更脏、更难、也更像工程的问题:

  • 你有没有划清模型和系统的边界
  • 你敢不敢完全相信模型生成的参数
  • 多步调用时你有没有刹车机制
  • 工具结果有没有被治理成“模型可用的证据”
  • 你有没有评测闭环去证明系统是真的在变好

如果这些问题没处理好,Function Call 就很容易沦为一个华丽但脆弱的功能展示。

只有把这些都补齐,它才会真正变成 AI 系统里的基础设施。

很多人以为 Function Call 解决的是“模型怎么调工具”,其实它真正解决的问题是:模型如何以一种可控、可验证、可治理的方式,开始接管现实世界里的动作。

这也是为什么,真正做过生产 AI 系统的人,聊起 Function Call 时,几乎从来不会只聊 Prompt。

他们更关心的,永远是边界、失败、治理和闭环。

END

写在最后:

最近私信问我面试题的小伙伴实在太多了,一个个回有点回不过来。

我大家公认最容易挂的 AI/Go/Java 面试坑点 整理成了一份 PDF 文档。里面不光有题,还有解题思路和避坑指南。

想要的同学,直接加我微信wangzhongyang1993,或者关注并私信我 【面试】,我统一发给大家。

前言

最近,是大家大规模找实习的时候。在这个时候,可能是大家第一次找实习,找工作,内心充满了各种疑惑。

对于大家的这种困惑,我把之前26届当时找实习问我的各种问题,给大家汇总出来了,希望能够对大家正确的职业思考规划有帮助

(这是不同时间写的,到时候思路策略肯定会有不同,具体有疑惑的可以私信哈)

(比如里面有的文章是去年9月份秋招写的,那哪个时候的策略肯定和四五月份是不一样的)

相关方向的初创公司的实习值得去吗,是不是学到东西的概率更大?

初创公司值不值得去,能不能学到东西。

其实可以首先对什么样的实习公司,什么样的公司实习排序。然后值不值得,在针对目前自身一个情况进行分析。
实习最主要加分的公司无非就是那些大厂、知名厂、以及一些行业的龙头企业

毕竟大家毕业也都是想去这些公司嘛,知名公司可以拿的出手,并且薪资也高。如果这个求职的时候,大家有过一段同等级的公司的实习,面试官会认为你已经被同级水平的公司筛选了并且通过了,说明你是有一定能力了,所以会很大程度给你面试,并且面试也应该不会太难。

当然,并不是所有同学都能拿到大厂实习offer,大多数都是拿一些中小厂的offer,那这个时候就会犹豫值不值得去了。这是就需要不同的情况,针对性分析了。

如果你是大一大二或者研一,大四保研的了,我认为可以去,一是可以去涨涨经验,给简历加加分 (实习一般都会加分,没有说实习还是减分这一说,只不过是加多加少罢了),二是去真是体验体验工作,看看这一行的强度自己是否能接受,如果不能接受,那可以提前考虑别的出路了。

如果你现在是大三或者研二,这个时候值不值得去,就需要看你目前基础准备的怎么样了,如果自我感觉准备的很好了,那就可以去。如果基础还差很多,马上都秋招了,还是专心搞基础吧,这才是关键,先保证自己起码能找到工作。

然后咱们再思考,实习能不能学到东西。其实首先,咱们考虑下,公司对实习生这个岗位是怎么定义呢,就知道能不能学到东西了。实习生这其实就是个临时工,毕竟你想走随时都可以走,这个时候如果你是老板,你会把核心的开发任务交给一个临时工来干吗。

那百分之百不会啊,所以说,不管去什么公司实习,其实都是学不到东西呢。

那企业为什么会招实习生啊,无非就是便宜免费劳动力一样,然后干干杂活多好哈哈哈。

那我个人去实习了又想让它发挥出应有的价值怎么办吧?
发挥出应有的价值其实就是简历上可以写一些含金量的东西,这个只能自己“偷”。

对看项目文档,看项目代码,针对性的看。脑海里有有一个想法,就是这个可以写在我都简历上吗,先初步预判下如果能,那就深入的学习。

了解下这个功能开发的需求原因,方案的调研设计,开发的逻辑,注重自己的文档总结,不然你看完不久就忘记了。梳理一份好的文档,就是让面试官针对这个简历写的点的考察,都可以在你都文档里面找到答案。

大厂侧开实习和小厂开发实习怎么选

先说选择,这个可以百分百确定选大厂,title很重要。
要想弄清楚那个选择对自己最有利,可以思考下实习的意义是什么?

实习无非就是给简历加分,拿到好offer,高薪offer。
那这就需要思考,简历怎么让面试官认可,那肯定是有个知名厂的实习,面试官看到你在知名厂呆过,面试官会认为你已经被同级水平的公司筛选了并且通过了,说明你是有一定能力了,所以会很大程度给你面试,并且面试也应该不会太难。

这个时候肯定会有人疑问,说我这是侧开实习,那秋招找正式工作的时候还能找开发吗

(1)简历是可以包装的,进去以后可以多做一些开发的任务,“偷”点开发的东西,多开发点自动话脚本;

(3)就是真去实习了,测试部门不能看到开发的代码,那也问题不大的。重点积累测试的内容也是可以的,重点是要有含金量。title是大于岗位的,同时下去继续学习开发的东西就可以了。

总而言之,就是让面试官知道你已经被大厂筛选过了,并且还通过了,这才是真正的加分点呢。

学历一般,没有实习,cpp学什么简历上可以增加含金量

这是星球同学在周五答疑上进行的询问,估计也有很多同学有这种困惑,学历一般,也没有对应大厂实习。但是依旧想秋招找个好工作不知道从何入手。

要想找个好工作,我们要首先理解清楚找工作的本质。找工作其实就是你投一份简历到公司,用人部门看到你的简历认为如果你简历上的都会了就可以胜任工作,然后给你发面试,面试通过发offer。

所以找工作最关键的就是要有一份不错的简历。

写一份不错的简历,首要要知道简历主要有哪几部分构成,针对可操作的部分改成最有含金量的尽力即可。

简历主要构成:

1.基本信息(个人信息、学历)

2.实习经历(并不是所有人都有,可有可无)

3.项目经历(一般都有,不然简历太空了,除非你有很多实习经历可以填补简历的空缺)

4.技术栈(让人知道你具体会什么,符不符合要求)

5.校园经历/个人评价(有没有参加过比赛获奖、以及展现自己对技术的热忱)

通过个人情况,一般我们可以包装的就是3、4、5点。重点能包装的其实就是项目经历和技术栈

项目无非就是放点高质量的项目(切记别把一个demo当作一个项目,比如实现了线程池内存池,以及rpc这放到技术栈里是个加分项,当作一个项目太简单了,规模太小),如果不知道做什么项目好,可以看看星球的互联网的项目以及底层项目

技术栈,这部分多写一些底层的知识,一是增加含金量;二是凸显出差异化,给面试官眼前一亮的感觉

如果自己学历一般,也没有大厂实习,技术栈也写的习俗平常,那怎么可能要你啊。

比如一个岗位招10个人,可能1000人投简历,然后面100人。这100人怎么筛选呢,无非就是学历好不好,有没有大厂实习。

如果自己这两个都没有,那就包装简历内容,让面试官看到你的简历发现你写的东西和大多数人都不一样,并且还很底层,让他对你产生兴趣,对你有了兴趣,才有面试的可能。有面试了能不能拿下就看你的实力了。

比如,计网的书写,一方面可以写大家都写的那部分内容(比如熟悉htt/https,熟悉udp、tcp三次握手四次挥手等),让面试官知道你基础能力也达到了合格水平

另一方面,可以写写星球为大家总结底层网络的相关知识点,给面试官眼前一亮,甚至屌爆他,让他对你产生极大的兴趣,这机会不就来了嘛。

校园经历,这个有参加过一些含金量的计算机相关比赛比较好,如果没有那只能多往展现自己对计算机热忱的事情上靠拢了,比如喜欢看计算机相关书籍,喜欢看源码等。凑凑字数

写给即将大三或者研二的同学的,真挚的建议

现在大家都知道,计算机编程这个行业内卷及其严重,导致现在招聘要求越来越高。如果想找一份好工作,想找一份可以进大厂的工作,那就需要在秋招春招的时候有一份非常出色的简历。

一份简历的构成,主要如下几个元素:

个人信息、校园经历、实习经历、项目经历、技术栈

个人信息(学历)这是已经确定无法改变的。校园经历,主要就是有没有含金量的比赛。如果你搞算法就是有没有顶会之类的。针对含金量的比赛,一般就是ACM,或者一些机器人大赛,这一般都是大一大二就开始准备备赛了,目前还没有准备,估计也准备不及了,也没有准备的必要了,所以这部分也基本很难改变了。

目前我们能做的就是改变实习经历、项目经历、技术栈。
项目经历、技术栈这部分,现在网络信息这么发达,大家获取的学习资料其实都差不多,所以这部分大家会发现其实写的也都差不多。

当然,有同学会说了,星球不是有独家资料嘛,这些资料我看外面都没有啊,我学这个不就可以超过别人了嘛。当然学这些,肯定可以超过外面同届或者超过面试官的水平。但是星球的同学都可以获得啊,那你怎么超过星球同学的水平哈。

所以这个时候有一段实习经历,一段大厂实习经历,会让自己简历很加分。尤其哪些学历不是特别好的同学,会很加分,会在一定程度上极大弥补学历的不足。

所以我今天要讲的是要找实习,要找大厂实习。怎么才能比较容易的找到大厂实习呢。其实面试一般不难,只要基础过关,一般都可以通过,实习面试通过就可以发offer。

关键是怎么才能更加容易的拿到面试?

答案是错峰竞争

根据这几年的辅导经验,以及同公司hr聊天,发现大家投实习一般都是在大三下或者研二下,即今年过完年立春之后陆续开始投递,也就是大家俗称的暑期实习。

这个时候大家投递热情很高涨,投递的人多了,那也就意味着难度变大了,毕竟供大于需了嘛。

所以我给大家写这篇文章,就是让大家把思路转化过来,不要一直随大流按部就班的来,如果这样来导致自己不管在什么阶段竞争压力都很大,及其容易出现努力很久,最终也是颗粒无收的状态。

那应该怎么办的,那就是错峰竞争,同时比别人提前多走一小步。

具体建议就是马上大三或者研二的同学,一定要在这个寒假找找实习,这个时候比较容易。为什么会这么说的?

一、就是目前现在在岗实习的学生大多数都是要今年秋招的,等秋招开启的时候,会有很多同学离职全力进行秋招,导致企业空缺出大量的实习hc

二、今年冬天投递实习的同学相对较少,很多人都是受网络的影响,一直认为明年投递才合适,并且也一直认为准备不好不敢投递,身边投递的少,就更加不敢投递了。
这个时候及其容易上岸,这就是错峰竞争。

什么是比别人提前多走一小步呢?可以看下面图片(领先一小步,就会容易很多)

多走一小步,后面每一步都会容易很多

可实习时间短,怎么做来能最大限度的发挥出实习的含金量

最近星球,有同学拿到了大厂的实习,但是可能因为学业的原因,可实习的时间相对较短,就问。在有限的时间内,怎么做,才能最大程度的发挥这段实习的含金量,放在简历上。估计这也是很多实习同学的一个疑惑。下面是他的原文:

在有限的时间内,最大发挥出实习的价值。可以理解为两点:

(1)简历可以写点含金量的东西

(2)针对面试实习的询问都可以应答上来

简历写含金量的东西

短时间内,让简历真实的写一些自己实习开发的功能点,估计不太现实。首先你是学生,还是实习生。一个部门老大怎么会把有含金量的开发任务给一个实习生呢,万一开发一半你跑了咋办,谁接手呢。找实习,就是让来打杂的。

那该怎么办呢,怎么“偷”点含金量的东西写上去呢?

1.可以先看看部门文档里有没有讲某个功能,讲这个功能开发的需求,功能实现的方式,然后找到对应的patch,如果看不懂就让ai给你解释,然后自己整理成文档。

2.如果在部门文档里找不到开发某个功能的文章讲述。那就看部门和你这个方向一样人提交的代码,看提交task任务的代码(不是fix bug的),然后点开他开这个任务的页面的描述,以及他提交的patch,看懂了,或者让ai解释,梳理成文档。(这是功能实现逻辑的梳理)。但是你还要明白,为什么要开发这个(需求),以及为什么这样做(方法的调研),这部分如果自己不懂,就趁吃饭或者散步的时候直接问开发这个任务的人就可以,一般都会给你解答的

及文档的梳理,至少包含三部分,开发的需求,设计思路,功能逻辑

针对实习面试的询问都能答上来

实习面试不仅仅会问你简历上写的这几点。还会宏观上拷问你,问你开发流程,问你整体的架构等等,这部分怎么掌握呢

1.首先快速梳理下部门的开发流程,及工作的方向,以及用的技术。这是聊实习最开始先唠嗑的点,如果这都答不出来就尴尬了

2.对项目架构整体了解了解,比如你这块负责的方向主要有几个进程啊,每个都是干什么的啊,彼此之间有什么联系啊。每个进程是怎么一个架构啊。

上述两点主要是用来让你在面试的时候,问你实习的整体情况,也就是闲聊发散性问的时候能答出来。(这部分快速掌握的方法就是看部门的文档,一般文档里都有写)

找实习,面试官告诉我至少要实习半年,但是我实际只能实习一两个月怎么办?

最近答疑的时候,有同学问我,就是说“面试官面试官告诉我至少要实习半年,但是我实际只能实习一两个月怎么办?

如果到时候实习一两个月就离职,会不会不好,会不会被这家公司拉黑,对我求职有不好的地方”

对于同学问出这种问题,只能说一直生活在这校园相对单纯的环境,没经历社会,还相对比较单纯。

因为这种问题找实习的时候会经常被问到,对于这问题,我们从两方面进行分析一下。

第一方面,就是努力消除大家心中“负罪感”

第二方面,就是对于这问题的回答的一个固定答案。

消除心中的“负罪感”:

(1)大家可以在网上看看,看看每年都有多少厂在裁应届生,努力那么久,刚进来没多久,没技术,工作经历时间短,就让应届生背指标,骗补贴。这种公司良心何在,责任感何在。(应该众所周知的厂子都有这种案例) 所以说,人家干你的时候毫不手软。 那自己也要时时刻刻为自己的前途考虑,思考多站在自己的未来角度思考,怎么有利于自己未来就怎么来

(2)你自己实习一段时间,简历可以写大厂经历加分。那离职了,空出来hc,是不是又要继续找,给哪些没有实习经历的同学一个机会。也是变相的帮助一下,素未谋面的,一起为着相同目标努力奋斗的战友,也算是做了一件好事

回答固定的答案:

(1)至少实习半年以上,自己感觉如果刚进入没多久就离开,其实对自己的技术提升为乎甚微。如果想技术提升,还是要长时间的磨练呢。自己好不容易,进入了咱们公司,咱们部门,能够有这么多优秀的同事,还是想踏下心来,好好提升提升技术,并且也为咱们部门做出自己的贡献呢

(2)每周的实习工作时间,和部门的同事一样。对加班也是接受的,毕竟新人刚入职自己还是很菜的,自己认为适当的加班还是很有必要呢

(主打一个舔,把offer骗到手,从被动变成主动,说他想听的答案)

本文由mdnice多平台发布

在住宅代理的实际应用中,网络连接的稳定性和响应速度直接影响业务效率。准确识别延迟与抖动的差异,是进行有效网络优化的前提。

网络延迟与抖动的核心区别

住宅代理场景中,网络延迟指数据包经代理节点往返目标服务器的平均耗时(单位:ms),是对 “响应快慢” 的直观衡量。抖动则是连续数据包传输延迟的波动幅度,反映连接 “稳定性”。

高延迟会导致访问响应迟缓,高抖动则会引发操作卡顿、数据传输断续,二者成因与优化方向完全不同。

延迟过高的检测与优化

检测延迟可通过ping命令测试代理节点与目标服务器的连通耗时,或用tracert定位路由瓶颈。优化时,优先选择邻近目标服务器的住宅代理节点,缩短物理传输路径;选用专业服务商的低负载优质节点,避开高峰拥堵;同时将本地网络切换为有线连接,减少无线干扰,降低基础延迟。

抖动异常的排查与优化

排查抖动问题需要持续观察延迟波动情况,当波动幅度超过 20 毫秒时,往往会直接影响业务的稳定运行。想要改善这类问题,最实用的方式是选用链路稳定、节点质量高的住宅代理,从源头减少网络波动。

此外,在使用时优先选择SOCKS5协议,相比传统 HTTP 协议,它能有效降低额外数据开销,减少连接不稳定情况,让整体网络传输更加流畅连贯。

值得关注的是,LokiProxy支持国家/州/城市精准定位,用户可选择与目标服务器同城的出口节点,最大程度缩短物理传输距离。当用户遇到延迟异常或连接问题时,LokiProxy还提供专业客服,可快速为您排查问题和提供链路调整建议。

总结

延迟考验的是“快不快”,抖动决定的是“稳不稳”,二者不可混为一谈。LokiProxy建议您:在实际业务中,先通过持续测试明确自身网络瓶颈所在,从准确诊断到精准优化,才能让住宅代理真正发挥稳定、高效的价值。

VK0256是一个点阵式存储映射的LCD驱动器,可支持最大
256点(32SEG×8COM)的LCD屏。单片机可通过3/4线串
行接口配置显示参数和发送显示数据,也可通过指令进入省
电模式。
• 工作电压 2.4-5.2V
• 内置32KHz RC振荡器(上电默认)
• 可外接32KHz时钟源
• 偏置电压(BIAS)为1/4
• COM周期(DUTY)为1/8
• 内置显示RAM为32x8位
• 蜂鸣器频率可配置为2kHz、4kHz
• 省电模式(通过关显示和关振荡器进入)
• 时基和看门狗共用1个时钟源,可配置8种频率
• 时基或看门狗溢出信号输出脚为/IRQ脚 (开漏)
• 3/4线串行接口
• 软件配置LCD显示参数
• 写命令和读写数据2种命令格式
• 读写显示数据地址自动加1
• VLCD脚调整LCD输出电压(≤VDD)
• 封装
QFP64(20.0mm × 14.0mm PP=1.0mm)

应用领域:
• 电表/瓦斯表
• 按摩仪/美容仪
• 医用仪器
• 车载设备
• 冷气机/暖风机

RAM映射LCD控制器和驱动器系列:

VK1024B 2.4V~5.2V 6seg×4com 6×3 6×2 偏置电压1/2 1/3 S0P16 省电模式

VK1056B 2.4V~5.2V 14seg×4com 14×3 14×2 偏置电压1/2 1/3 SOP24 省电模式

VK1056C 2.4V~5.2V 14seg×4com 14×3 14×2 偏置电压1/2 1/3 SSOP24 省电模式

VK1056Q 2.4V~5.2V 14seg×4com 14×3 14×2 偏置电压1/2 1/3 QFN24(4×4超小体积) 省电模式

VK1072B 2.4V~5.2V 18seg×4com 18×3 18×2 偏置电压1/2 1/3 SOP28 省电模式

VK1072C 2.4V~5.2V 18seg×4com 18×3 18×2 偏置电压1/2 1/3 SOP28 省电模式

谷歌发布了Aletheia,这是一套基于 Gemini 3 Deep Think 的 AI 系统,在FirstProof 挑战中成功解决了 10 道全新数学问题中的 6 道。与此同时,Aletheia 在IMO-ProofBench上取得了约 91.9% 的成绩,这表明在无需人工干预的研究级证明自动发现方面出现了显著进展。

 

与传统基准测试常见的数据污染问题(即模型无意中记住训练数据)不同,FirstProof 挑战包含了十个尚未发表的研究级数学引理。这些问题来源于数学家正在进行的研究工作,从未在网上发布,因此几乎可以确定 AI 不可能提前见过这些题目。此外,参赛者仅有一周时间提交解答。

 

在仅提供原始问题描述、没有任何人工提示或交互循环的情况下,Aletheia 完全自主地生成了候选证明。由人类专家组成的评审团队认为,其中 10 个解答中的 6 个“经过少量修改即可发表”。值得注意的是,第 8 题的解答获得了 7 位专家中 5 位的认可为正确,其余专家则指出缺少一些必要的澄清细节。更关键的是,对于剩余的 4 道问题,Aletheia 明确输出“未找到解”或因超时未给出结果,而不是生成看似合理但存在缺陷的错误答案。DeepMind 研究人员对此表示

这种自我过滤能力是 Aletheia 的关键设计原则之一;我们认为,在将 AI 扩展应用于研究级数学时,可靠性是主要瓶颈。我们推测……许多在一线工作的研究者更愿意用更高的准确性来换取一定程度的问题求解能力。

 

OpenAI 也使用一款尚未发布的内部推理模型参与了该挑战。最初他们报告称解决了 10 道问题中的 6 道(具体为第 2、4、5、6、9 和 10 题),但随后在发现第 2 题的解答存在逻辑缺陷后,将结果下调为 5 道。与 DeepMind 严格的零样本自动化不同,OpenAI 承认在过程中依赖了有限的人类监督,用于从多次尝试中手动评估并筛选最佳结果

 

在底层实现上,Aletheia 基于 Gemini 3 Deep Think 架构,并依赖扩展的“测试时计算”(即推理阶段的计算资源)。系统采用多代理框架,包括用于提出逻辑步骤的 Generator、用于检测步骤缺陷的 Verifier,以及用于迭代修正错误的 Reviser。通过整合如谷歌搜索等外部工具,该代理能够检索现有文献以验证相关概念,从而降低大语言模型常见的无依据引用问题。

 

Aletheia:基于 Gemini Deep Think,展示多步骤解答验证流程。(来源:谷歌 DeepMind 博客

 

正如Luhui Dev 在一篇深度分析中所指出,Aletheia 本质上类似一个严格且可执行的研究循环,可以类比为数学领域的 CI/CD 流水线:提出、验证、失败、修复、合并。在这一过程中,大语言模型充当具有创造力的候选生成器,而第二个代理则类似同行评审者,推动问题修复。

 

不过,研究人员在论文《Towards Autonomous Mathematics Research(迈向自主化数学研究)》中也指出,尽管短短数月内已经取得显著进展,但完全自主化仍未实现:

即便引入了验证机制,Aletheia 在出错率上仍高于人类专家。此外,只要问题存在一定歧义,模型往往倾向于将其理解为更容易回答的形式……这与机器学习中众所周知的‘规格博弈(specification gaming)’和‘奖励劫持(reward hacking)’现象是一致的。

 

该项目背后的数学家团队已经在推进第二版系统。下一批问题将在 2026 年 3 月至 6 月期间设计、测试并评估,并计划构建为一个完全形式化的基准测试体系。

 

Aletheia 由 Gemini Deep Think 的高级版本提供支持。

谷歌的 Java 版 Agent Development Kit(ADK)正式发布 1.0 版本,新增外部工具集成、新的应用与插件架构、高级上下文工程、人机协同工作流等功能。

新增支持的工具包括:GoogleMapsTool(提供谷歌地图数据访问能力)、UrlContextTool(可直接获取并总结网页内容)、ContainerCodeExecutorVertexAICodeExecutor(支持通过 Docker 容器在本地或借助 Vertex AI 在云端安全稳定地执行代码),以及 ComputerUseTool(可操控网页浏览器或计算机,需集成 Playwright)。

为了让智能体工具的交互能力更加强大,ADK 1.0 新增了两个 API:App 类作为智能体应用的顶层容器,负责托管根智能体、存储全局配置并处理插件集成;Plugins 用于定义扩展能力的基类。同时提供多个开箱即用的插件,包括用于结构化记录智能体执行与工具调用的 LoggingPlugin、通过过滤历史对话轮次管理上下文窗口的 ContextFilterPlugin,以及用于向所有智能体应用统一动态下发指令的 GlobalInstructionPlugin

新版本还提供了一个实用功能:事件压缩(Event Compaction)。该功能通过“仅保留最近事件的滑动窗口和/或对历史事件进行摘要”来控制智能体的上下文大小。作为一项基础的上下文工程实践,它能有效避免上下文窗口超出词元限制,同时降低长时运行会话的延迟与成本。

事件压缩 API 允许开发者对压缩间隔、词元阈值、重叠大小、事件保留限制以及所用摘要器进行灵活配置。

该框架还内置了对人机协同工作流的支持。借助新版 ADK,智能体可在执行关键操作前暂停执行、请求人工批准,获得确认后再继续执行。

已注册的工具可访问 ToolContext 并调用 requestConfirmation(),这会自动中断执行流程,暂停 LLM 运行直至获取输入。……ADK 会自动清理中间事件,并将已确认的函数调用显式注入后续的 LLM 请求上下文,确保模型知晓该操作已获批准,避免陷入执行循环。

ADK 还原生支持 Agent2Agent(A2A)协议,让不同智能体可以相互通信、协作,即便它们基于不同的编程语言或框架实现。为此,ADK 已集成官方的 A2A Java SDK 客户端

若要通过 A2A 协议对外暴露你自己的 ADK 智能体,需要创建一个 A2A AgentExecutor。它会对 ADK 智能体进行封装,并通过 JSON-RPC REST 接口对外暴露,将你的 ADK 智能体接入更广泛的 A2A 生态系统。

谷歌同时为 Java 和 Python 提供了 Agent Development Kit。在数月前发布 Java 版 ADK时,该项目主要贡献者 Guillaume Laforge 曾在 Reddit 上澄清:“新功能与新实验通常先在 Python 上落地,再逐步移植到 Java”。近期,Reddit 用户 Qubit99 建议开发者优先使用 Python 版而不是 Java 版 ADK,理由是对谷歌自身 Java 库维护情况的担忧。而 José Carlos Sancho 则在 X.com 上表示,Java 的类型安全是一项显著优势,能够在早期发现编排错误,这让 Java 智能体在大规模运行场景下比 Python 版本更稳定。

Java 版 ADK 可以从 GitHub 下载

【声明:本文由 InfoQ 翻译,未经许可禁止转载。】

查看英文原文:https://www.infoq.com/news/2026/04/google-adk-1-0-new-architecture/

应该是昨天晚上 10 点多(不是 8 点多)开始的,今天早上 6 点多才恢复。

https://status.aliyun.com/#/eventDetail?eventId=34

[异常(已恢复)] 韩国(首尔)地域可用区 B 部分云产品服务异常

事件进展

06:25 受影响云产品服务均已全部恢复。

05:38 网络设备在机房完成上电后已优先恢复运行,受影响云产品服务正在逐步恢复中。

北京时间 2026 年 04 月 22 日 03:56 机房制冷系统已逐步恢复运行,环境温度正在逐步回落中。工程师将持续监控设备运行状态并推进后续恢复工作。

22:25 工程师已根据应急预案对受影响云产品执行关机保护操作,目前冷却系统异常仍在紧急处理中,有最新进展将第一时间同步。

尊敬的客户:您好!北京时间 2026 年 04 月 21 日 20 点 06 分,阿里云监控发现韩国(首尔)地域可用区 B 包间制冷系统出现异常。工程师已根据应急预案对受影响云产品执行迁移、关机等操作,期间部分产品服务将受到影响。请您密切关注业务恢复情况,若有任何问题,请随时联系我们。

xy 和 pdd 上好像有很多美版的,要便宜好几千,这种机子有问题吗?是怎么回事?老哥们有靠谱店铺推荐一下更好

最近想买一个笔记本做开发机,刷到闲鱼的 es 工程机好便宜,大概知道可能有 bug ,看网上评论踩雷的不多。

联想小新 pro 14 imh9 2024 款的,处理器是 u7 155h ,R7 8845h ,32+512 配置的价格 2900

我现在每天回家就是打游戏 以前还打打联盟 现在沉迷手游和平暖暖 越打越上头
以前还偶尔学习一下 但现在每天八点半到家 打到十点半十一点 然后直接睡觉

某考公做题软件是收藏的历年真题、考点解答之类的
他们应该是收集后人工清洗了一遍。
本来是付费的,结果他们的软件贼卡贼难用,反馈客服只会让我清理电脑。我就想着看能不能把我付费的科目保存下来,
结果发现他们其余课程接口只做了前端鉴权。。。就顺手都扒了下来

这玩意有版权吗?我直接搞个 web 不收费共享人类没毛病吧?还是说我使用最好再自己混编一遍?

上个月底写的,类似扫雷的小游戏:
https://blog.mybatis.io/post/hormuz-game

🚢 游戏背景
霍尔木兹海峡是波斯湾通往阿拉伯海的咽喉要道,全球约 20%的石油经此运输。如今,海峡水域布满水雷,你的任务是驾驶货轮找到一条安全航道,成功穿越封锁。

🎮 操作方法
左键点击水域格子:探测该区域,如果安全则显示周围水雷数量,货轮将自动沿新开辟的安全路径前进。
右键点击水域格子:标记/取消标记疑似水雷位置(旗帜标记)。
数字提示周围 8 格中的水雷数量,利用这些信息判断安全路径。
货轮将自动沿从左到右的最短安全路径行进,到达右侧出口即为胜利。
⚙️ 难度说明
低( 35 枚水雷):水雷稀疏,安全区域宽阔,适合熟悉游戏机制。偶尔有导弹威胁。
中( 55 枚水雷):标准难度,需要谨慎探测才能找到安全航道。导弹频率提高。
高( 80 枚水雷):水雷密布,右侧出口受限,导弹追踪更密集,考验快速判断能力。



地址: https://blog.mybatis.io/post/hormuz-game

这是 github👈
chrome 应用商店上找要么是要注册和 AI 结合的过于复杂的,要么是只能记录单页的。
都不太符合我的要求,所以花了几分钟 vibe 了一个,使用几天感觉还不错,对于轻量化记录来说还挺方便的doge

该说不说 4.7 这审美真不错,我就丢了个 PRD 给他,生成的 UI 看着真不赖

前情回顾:

一年多没联系的大学女同学突然找我帮忙办事,重新联系了起来。随后我们连续约了三次见面:从吃饭散步,到小酌聊天,再到去她家放包、一起唱 K 。相处过程轻松自然,彼此不排斥,甚至有一些靠近的信号,但我始终犹豫,没有迈出更进一步的肢体接触。
具体可以看:https://www.v2ex.com/t/1201054


后续:

这个后续,我想我拖了很久。其实不太愿意交代后面的事情,但想到上一次有很多兄弟在等更新,还是写一下。

在写这个后续之前,我也陷入了大思考。现在的相处方式,真的已经变得这么浮躁了吗?

从上次唱 K 结束之后,我们中间一直都有联系。但期间有三个小插曲,让我感到不太舒服。


第一个:

月初某个工作日,我约她吃饭。因为我在越秀,她在番禺,我就说让她来越秀。我的考虑是,如果她没上班,过来可以早点吃饭;而我下班再去番禺,可能要一个多小时。

我说:那你来越秀,请你吃饭。
她回:会有惊喜吗?最近喜欢阿玛尼的兰岩草香水。

我的大小姐,这也太直接了吧。

这让我“肥肠虾头”。我不太清楚别的男生怎么想,但在我看来,如果关系自然推进,我是知道该怎么做的;
但这种直接表达,会让我不太舒服。

最后的结果是,我还是去了番禺和她吃饭。


第二个:

有一次我在上班。可能是我平时比较常找她聊天,她就觉得我比较闲,于是让我帮她做 PPT 。她最近在面试,可能需要试讲。

她问我:闲着吗?闲着用 AI 帮我做个 PPT 。
我还没答应,她就已经把资料发过来了。

我的大小姐,我在上班诶,给我的感觉就是我有点被当成工具人。

后来我想想也没什么,就用豆包帮她做了一个 PPT 。
做完之后我打趣说:帮你这么大忙,不得请我吃顿饭?


第三个:

因为我帮她做了 PPT ,所以我提出让她请我吃饭。于是我们又去了番禺。

但这顿“她请的饭”,过程让我非常难受。

我们到了一个商场,她问我吃什么。我说可以先逛一逛。
到了负一楼,她带我逛了很久。我说楼上好像有家烤鱼,可以吃烤鱼。她看了美团,说有优惠券,于是我们上楼。

上去之后她说,这个优惠券中午才能用,晚上不能用。
我内心 OS:那我大概明白了。

我说那就换一家。于是又走了很久。
最后我提议吃粥底火锅,这个我之前一直很想吃,还没吃过。

她说这个贵,而且没什么东西吃的。
来回拉扯了很久,整个过程都很纠结。

最后我直接说:就吃这个吧,我想吃这个,我请你。

结果,这顿本来说好她请的饭,最后还是变成我请她。

在这之前,我们一共吃了六次饭、两次唱 K ,都是我买单。我觉得她请我吃一顿,并不过分。

我也在想,可能是她现在待业,所以比较节俭。但整个过程带给我的感受确实不太好。我认为,情绪价值也是一种价值,不应该因为一些优惠的问题反复拉扯,这样其实挺不体面的。


基于以上三个插曲,其实让我很不舒服,我逐渐意识到,我们大概率走不到一起。
可能是消费观不太合得来?但我再往下想,可能不完全是这个原因。

更核心的是,在一些具体互动里,我感受到自己被放在一个相对次的位置
与其说是消费观不一致,我更看重人本身和相处体验,而她在一些场景下表现得相对功利,这让我很不舒服。


来吧,我想评论区应该会出现很多乌龟表情包,会有很多评论“龟男”。
其实我确实应该在兰岩草的那个点,就不再和她继续了,但我不确定兰岩草这个点她是不是开玩笑,所以才有了后面的相处,后面发生的事更加确定了她不是我需要找的人。

唉,就这样吧。

现在的相处方式真的很浮躁。

但我应该不会因为这个女生,就对下一段关系提前带上偏见。
每一个出现在我人生里的女孩,我依然会认真对待。

老妈已经退休 4 年了,单身,退休之前是护士,退休工资对于生活来说完全足够。
但老妈常常想找点挣钱的事情做,尝试过卖保健品,体制内上班惯了,不擅长销售,效果不好,反而产生挫败感。
前段时间还去美容院帮人按摩,体力劳动消耗太大,会引起劳损,累死累活一天也就几十块,商量下来还是不做了。

其实老妈退休算是有钱有闲,一年可能会和姐妹出去 3 ~ 4 次的自驾旅行,每周 2 节老年大学声乐课程,但还是一直想赚点钱。
在我打工人的角度,这是我向往的生活,而老妈退休了整天还想着打工。
分析下来,老妈没有伴侣,也没有想找伴侣,我也没有孙子给她带,缺乏一起消磨时间的伴侣、消磨时间的兴趣爱好,总之可能是缺乏一种正反馈或者说是价值感吧。

不知道各位 V 友有没有适合退休人士做的事情或者是兴趣爱好,最好满足:
1.时间自由,因为经常也要出去旅行
2.能挣点小钱最好,不能挣的话,能够有益身心健康也可以
3.不是重体力劳动

我想了下,炒股竟然能满足以上条件,但是有门槛,不放心老妈去尝试。

想做大型协作性项目,不想让 AI 偏离设计文档,所以要上一个 spec 系统做唯一的真相来源,网上不少推荐 openspec+superpowers ,都是说 openspec 做 proposal 跟 archive ,让 superpowers 做实现,但是没一个说到底怎么提示的。 我的疑问是,openspec 的 proposal 会生成 proposal.mddesing.md 以及 task.md ,但是 superpower 的 brainstorm 以哪个为来源,他本身也会生成 spec.md ,plan.md ,这两个是不是需要合并到 openspec 生成的一些列文档中去。

网上不少的说明都每说这个事情,有人尝试两个一起用的么?

一、系统概述

本程序使用VC++ MFC对话框实现PC与单片机的串口通信,支持数据发送、接收、显示和存储功能,适用于工业控制、数据采集等场景。

二、实现原理

2.1 通信架构

graph LR
    A[PC端VC++程序] --串口--> B[51单片机]
    B --串口--> A
    A --> C[用户界面]
    C --> A

2.2 数据流

  1. PC发送控制命令到单片机
  2. 单片机执行命令并返回数据
  3. PC接收数据并显示在界面
  4. 用户可保存数据或发送新命令

三、完整实现代码

3.1 资源文件定义 (Resource.h)

#define IDD_SERIAL_COMM_DIALOG       102
#define IDC_MSCOMM1                  1001
#define IDC_COMBO_COM_PORT           1002
#define IDC_COMBO_BAUD_RATE          1003
#define IDC_BUTTON_OPEN              1004
#define IDC_BUTTON_CLOSE             1005
#define IDC_EDIT_SEND                1006
#define IDC_BUTTON_SEND              1007
#define IDC_EDIT_RECEIVE             1008
#define IDC_CHECK_HEX_SEND           1009
#define IDC_CHECK_HEX_RECEIVE         1010
#define IDC_BUTTON_CLEAR             1011
#define IDC_BUTTON_SAVE              1012
#define IDC_STATIC_STATUS            1013
#define IDC_COMBO_DATA_BITS          1014
#define IDC_COMBO_PARITY             1015
#define IDC_COMBO_STOP_BITS          1016

3.2 对话框类头文件 (SerialCommDlg.h)

#if !defined(AFX_SERIALCOMMDLG_H__)
#define AFX_SERIALCOMMDLG_H__

#include <afxwin.h>
#include <afxdisp.h>
#include <afxcmn.h>
#include <mscomm.h>

class CSerialCommDlg : public CDialog {
public:
    CSerialCommDlg(CWnd* pParent = NULL);
    enum { IDD = IDD_SERIAL_COMM_DIALOG };
    
protected:
    virtual void DoDataExchange(CDataExchange* pDX);
    virtual BOOL OnInitDialog();
    
    // 控件变量
    CMSComm m_mscom;
    CComboBox m_comboComPort;
    CComboBox m_comboBaudRate;
    CComboBox m_comboDataBits;
    CComboBox m_comboParity;
    CComboBox m_comboStopBits;
    CEdit m_editSend;
    CEdit m_editReceive;
    CButton m_checkHexSend;
    CButton m_checkHexReceive;
    CStatic m_staticStatus;
    
    // 消息处理
    afx_msg void OnButtonOpen();
    afx_msg void OnButtonClose();
    afx_msg void OnButtonSend();
    afx_msg void OnButtonClear();
    afx_msg void OnButtonSave();
    afx_msg void OnCommMscomm();
    afx_msg void OnClose();
    
    // 辅助函数
    void UpdateStatus(LPCTSTR msg);
    void ProcessReceivedData();
    CString FormatHexData(const BYTE* data, int len);
    void AddToReceiveBox(LPCTSTR str);
    
    DECLARE_MESSAGE_MAP()
    DECLARE_EVENTSINK_MAP()
};

#endif

3.3 对话框实现文件 (SerialCommDlg.cpp)

#include "stdafx.h"
#include "SerialComm.h"
#include "SerialCommDlg.h"
#include <afxmt.h>
#include <fstream>

// 事件映射
BEGIN_EVENTSINK_MAP(CSerialCommDlg, CDialog)
    ON_EVENT(CSerialCommDlg, IDC_MSCOMM1, 1, OnCommMscomm, VTS_NONE)
END_EVENTSINK_MAP()

// 消息映射
BEGIN_MESSAGE_MAP(CSerialCommDlg, CDialog)
    ON_BN_CLICKED(IDC_BUTTON_OPEN, OnButtonOpen)
    ON_BN_CLICKED(IDC_BUTTON_CLOSE, OnButtonClose)
    ON_BN_CLICKED(IDC_BUTTON_SEND, OnButtonSend)
    ON_BN_CLICKED(IDC_BUTTON_CLEAR, OnButtonClear)
    ON_BN_CLICKED(IDC_BUTTON_SAVE, OnButtonSave)
    ON_WM_CLOSE()
END_MESSAGE_MAP()

// 构造函数
CSerialCommDlg::CSerialCommDlg(CWnd* pParent) : CDialog(CSerialCommDlg::IDD, pParent) {
    m_mscom.m_bAutoSize = TRUE;
}

// 数据交换
void CSerialCommDlg::DoDataExchange(CDataExchange* pDX) {
    CDialog::DoDataExchange(pDX);
    DDX_Control(pDX, IDC_MSCOMM1, m_mscom);
    DDX_Control(pDX, IDC_COMBO_COM_PORT, m_comboComPort);
    DDX_Control(pDX, IDC_COMBO_BAUD_RATE, m_comboBaudRate);
    DDX_Control(pDX, IDC_COMBO_DATA_BITS, m_comboDataBits);
    DDX_Control(pDX, IDC_COMBO_PARITY, m_comboParity);
    DDX_Control(pDX, IDC_COMBO_STOP_BITS, m_comboStopBits);
    DDX_Control(pDX, IDC_EDIT_SEND, m_editSend);
    DDX_Control(pDX, IDC_EDIT_RECEIVE, m_editReceive);
    DDX_Control(pDX, IDC_CHECK_HEX_SEND, m_checkHexSend);
    DDX_Control(pDX, IDC_CHECK_HEX_RECEIVE, m_checkHexReceive);
    DDX_Control(pDX, IDC_STATIC_STATUS, m_staticStatus);
}

// 初始化对话框
BOOL CSerialCommDlg::OnInitDialog() {
    CDialog::OnInitDialog();
    
    // 初始化串口列表
    for (int i = 1; i <= 16; i++) {
        CString str;
        str.Format(_T("COM%d"), i);
        m_comboComPort.AddString(str);
    }
    m_comboComPort.SetCurSel(0);
    
    // 初始化波特率列表
    CString baudRates[] = {_T("1200"), _T("2400"), _T("4800"), 
                          _T("9600"), _T("19200"), _T("38400"),
                          _T("57600"), _T("115200")};
    for (int i = 0; i < 8; i++) {
        m_comboBaudRate.AddString(baudRates[i]);
    }
    m_comboBaudRate.SetCurSel(3); // 默认9600
    
    // 初始化数据位列表
    CString dataBits[] = {_T("5"), _T("6"), _T("7"), _T("8")};
    for (int i = 0; i < 4; i++) {
        m_comboDataBits.AddString(dataBits[i]);
    }
    m_comboDataBits.SetCurSel(3); // 默认8位
    
    // 初始化校验位列表
    CString parity[] = {_T("无"), _T("奇校验"), _T("偶校验")};
    for (int i = 0; i < 3; i++) {
        m_comboParity.AddString(parity[i]);
    }
    m_comboParity.SetCurSel(0); // 默认无校验
    
    // 初始化停止位列表
    CString stopBits[] = {_T("1"), _T("1.5"), _T("2")};
    for (int i = 0; i < 3; i++) {
        m_comboStopBits.AddString(stopBits[i]);
    }
    m_comboStopBits.SetCurSel(0); // 默认1位
    
    // 初始化MSComm控件
    if (!m_mscom.Create(NULL, WS_VISIBLE | WS_CHILD, CRect(0,0,0,0), this, IDC_MSCOMM1)) {
        AfxMessageBox(_T("无法创建MSComm控件!"));
        return FALSE;
    }
    
    // 设置默认参数
    m_mscom.SetCommPort(1);       // 默认COM1
    m_mscom.SetSettings(_T("9600,n,8,1")); // 默认参数
    m_mscom.SetInputMode(1);      // 二进制模式
    m_mscom.SetRThreshold(1);     // 每接收1个字符触发事件
    m_mscom.SetSThreshold(0);     // 不触发发送事件
    m_mscom.SetPortOpen(FALSE);   // 初始关闭
    
    UpdateStatus(_T("就绪"));
    
    return TRUE;
}

// 打开串口
void CSerialCommDlg::OnButtonOpen() {
    CString strCom, strBaud, strDataBits, strParity, strStopBits;
    m_comboComPort.GetWindowText(strCom);
    m_comboBaudRate.GetWindowText(strBaud);
    m_comboDataBits.GetWindowText(strDataBits);
    m_comboParity.GetWindowText(strParity);
    m_comboStopBits.GetWindowText(strStopBits);
    
    int nCom = _ttoi(strCom.Mid(3));
    
    // 转换校验位
    CString parityCode;
    if (strParity == _T("无")) parityCode = _T("n");
    else if (strParity == _T("奇校验")) parityCode = _T("o");
    else if (strParity == _T("偶校验")) parityCode = _T("e");
    else parityCode = _T("n");
    
    CString strSettings;
    strSettings.Format(_T("%s,%s,%s,%s"), strBaud, parityCode, strDataBits, strStopBits);
    
    // 设置串口参数
    if (m_mscom.GetPortOpen()) {
        m_mscom.SetPortOpen(FALSE);
    }
    
    m_mscom.SetCommPort(nCom);
    m_mscom.SetSettings(strSettings);
    
    // 打开串口
    if (!m_mscom.GetPortOpen()) {
        if (m_mscom.SetPortOpen(TRUE)) {
            UpdateStatus(_T("串口已打开: ") + strCom + _T(" ") + strSettings);
            GetDlgItem(IDC_BUTTON_OPEN)->EnableWindow(FALSE);
            GetDlgItem(IDC_BUTTON_CLOSE)->EnableWindow(TRUE);
        } else {
            AfxMessageBox(_T("无法打开串口!"));
        }
    }
}

// 关闭串口
void CSerialCommDlg::OnButtonClose() {
    if (m_mscom.GetPortOpen()) {
        m_mscom.SetPortOpen(FALSE);
        UpdateStatus(_T("串口已关闭"));
        GetDlgItem(IDC_BUTTON_OPEN)->EnableWindow(TRUE);
        GetDlgItem(IDC_BUTTON_CLOSE)->EnableWindow(FALSE);
    }
}

// 发送数据
void CSerialCommDlg::OnButtonSend() {
    if (!m_mscom.GetPortOpen()) {
        AfxMessageBox(_T("串口未打开!"));
        return;
    }
    
    CString strSend;
    m_editSend.GetWindowText(strSend);
    if (strSend.IsEmpty()) {
        return;
    }
    
    // 十六进制发送处理
    if (m_checkHexSend.GetCheck()) {
        CString strHex = strSend;
        strHex.Remove(' ');
        strHex.Remove('-');
        
        if (strHex.GetLength() % 2 != 0) {
            AfxMessageBox(_T("HEX数据长度必须为偶数!"));
            return;
        }
        
        int len = strHex.GetLength() / 2;
        BYTE* data = new BYTE[len];
        ZeroMemory(data, len);
        
        for (int i = 0; i < len; i++) {
            CString byteStr = strHex.Mid(i*2, 2);
            data[i] = (BYTE)strtol(byteStr, NULL, 16);
        }
        
        COleVariant var((BYTE*)data, len);
        m_mscom.SetOutput(var);
        delete[] data;
        
        AddToReceiveBox(_T("[发送] ") + FormatHexData(data, len));
    } 
    // 文本发送
    else {
        COleVariant var(strSend);
        m_mscom.SetOutput(var);
        AddToReceiveBox(_T("[发送] ") + strSend);
    }
    
    UpdateStatus(_T("数据已发送"));
}

// 清空接收区
void CSerialCommDlg::OnButtonClear() {
    m_editReceive.SetWindowText(_T(""));
}

// 保存数据
void CSerialCommDlg::OnButtonSave() {
    CString strData;
    m_editReceive.GetWindowText(strData);
    if (strData.IsEmpty()) {
        AfxMessageBox(_T("接收区为空!"));
        return;
    }
    
    CFileDialog dlg(FALSE, _T("txt"), _T("serial_data.txt"), 
                   OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT,
                   _T("文本文件 (*.txt)|*.txt|所有文件 (*.*)|*.*||"));
    
    if (dlg.DoModal() == IDOK) {
        CStdioFile file;
        if (file.Open(dlg.GetPathName(), CFile::modeCreate | CFile::modeWrite)) {
            file.WriteString(strData);
            file.Close();
            UpdateStatus(_T("数据已保存: ") + dlg.GetFileName());
        } else {
            AfxMessageBox(_T("保存文件失败!"));
        }
    }
}

// 关闭窗口
void CSerialCommDlg::OnClose() {
    if (m_mscom.GetPortOpen()) {
        m_mscom.SetPortOpen(FALSE);
    }
    CDialog::OnClose();
}

// 串口事件处理
void CSerialCommDlg::OnCommMscomm() {
    VARIANT variant_inp;
    COleSafeArray safearray_inp;
    LONG len, k;
    BYTE rxdata[2048]; // 接收缓冲区
    
    switch (m_mscom.GetCommEvent()) {
    case 2: // 接收事件
        variant_inp = m_mscom.GetInput();
        safearray_inp = variant_inp;
        len = safearray_inp.GetOneDimSize();
        
        // 数据复制到缓冲区
        for (k = 0; k < len; k++) {
            safearray_inp.GetElement(&k, rxdata + k);
        }
        
        // 处理接收到的数据
        ProcessReceivedData(rxdata, len);
        break;
        
    case 4: // 发送事件
        UpdateStatus(_T("数据发送完成"));
        break;
        
    case 5: // 错误事件
        UpdateStatus(_T("通信错误!"));
        break;
        
    default:
        break;
    }
}

// 处理接收数据
void CSerialCommDlg::ProcessReceivedData(BYTE* data, int len) {
    CString strDisplay;
    
    // 十六进制显示
    if (m_checkHexReceive.GetCheck()) {
        strDisplay = FormatHexData(data, len);
    } 
    // 文本显示
    else {
        // 转换为CString
        data[len] = '\0';
        strDisplay = CString((char*)data);
    }
    
    AddToReceiveBox(_T("[接收] ") + strDisplay);
    UpdateStatus(_T("收到数据"));
}

// 格式化十六进制数据
CString CSerialCommDlg::FormatHexData(const BYTE* data, int len) {
    CString strHex;
    for (int i = 0; i < len; i++) {
        CString byteStr;
        byteStr.Format(_T("%02X "), data[i]);
        strHex += byteStr;
        
        if ((i+1) % 16 == 0) {
            strHex += _T("\r\n");
        }
    }
    return strHex;
}

// 添加到接收框
void CSerialCommDlg::AddToReceiveBox(LPCTSTR str) {
    CString strOld;
    m_editReceive.GetWindowText(strOld);
    if (!strOld.IsEmpty()) {
        strOld += _T("\r\n");
    }
    m_editReceive.SetWindowText(strOld + str);
    
    // 滚动到最后一行
    int nLength = m_editReceive.GetWindowTextLength();
    m_editReceive.SetSel(nLength, nLength);
    m_editReceive.ReplaceSel(_T(""));
}

// 更新状态
void CSerialCommDlg::UpdateStatus(LPCTSTR msg) {
    m_staticStatus.SetWindowText(msg);
}

3.4 应用程序类 (SerialCommApp.cpp)

#include "stdafx.h"
#include "SerialComm.h"
#include "SerialCommDlg.h"

BEGIN_MESSAGE_MAP(CSerialCommApp, CWinApp)
    ON_COMMAND(ID_HELP, CWinApp::OnHelp)
END_MESSAGE_MAP()

CSerialCommApp::CSerialCommApp() {
    m_dwRestartManagerSupportFlags = AFX_RESTART_MANAGER_SUPPORT_RESTART;
}

CSerialCommApp theApp;

BOOL CSerialCommApp::InitInstance() {
    INITCOMMONCONTROLSEX InitCtrls;
    InitCtrls.dwSize = sizeof(InitCtrls);
    InitCtrls.dwICC = ICC_WIN95_CLASSES;
    InitCommonControlsEx(&InitCtrls);
    
    CWinApp::InitInstance();
    
    CSerialCommDlg dlg;
    m_pMainWnd = &dlg;
    dlg.DoModal();
    
    return FALSE;
}

四、单片机端示例代码 (C51)

4.1 串口初始化

#include <reg52.h>

#define FOSC 11059200L      // 晶振频率
#define BAUD 9600           // 波特率

void UART_Init() {
    SCON = 0x50;            // 8位数据,可变波特率
    TMOD |= 0x20;           // 定时器1工作方式2
    TH1 = TL1 = -(FOSC/12/32/BAUD); // 设置波特率
    TR1 = 1;                // 启动定时器1
    EA = 1;                 // 开总中断
    ES = 1;                 // 开串口中断
}

void UART_SendByte(unsigned char dat) {
    SBUF = dat;
    while(!TI);
    TI = 0;
}

void UART_SendString(char *s) {
    while(*s) {
        UART_SendByte(*s++);
    }
}

4.2 主程序逻辑

unsigned char receivedData[32];
unsigned char receiveCount = 0;

void main() {
    UART_Init();
    while(1) {
        // 主循环可以添加其他任务
    }
}

// 串口中断服务函数
void UART_ISR() interrupt 4 {
    if(RI) {
        RI = 0; // 清除接收中断标志
        
        unsigned char dat = SBUF;
        
        // 简单协议:收到0x01返回温度数据
        if(dat == 0x01) {
            // 模拟温度数据:25.5℃
            UART_SendByte(0x25); // 整数部分
            UART_SendByte(0x50); // 小数部分
        }
        // 收到0x02返回状态信息
        else if(dat == 0x02) {
            UART_SendString("System OK\r\n");
        }
        // 其他数据回显
        else {
            UART_SendByte(dat);
        }
    }
}

参考代码 利用串口和单片机进行通讯,VC用对话框实现 www.youwenfan.com/contentsfa123975.html

五、使用说明

5.1 系统要求

  • Windows XP/7/10/11
  • Visual C++ 6.0 或更高版本
  • MSComm控件 (mscomm32.ocx)

5.2 安装步骤

  1. 注册MSComm控件:

    regsvr32 mscomm32.ocx
  2. 在VC++项目中:

    • 添加mscomm.h和mscomm.cpp到项目
    • 在stdafx.h中添加:

      #include <mscomm.h>
      #pragma comment(lib, "mscomm.lib")

5.3 操作流程

  1. 选择串口号和通信参数(波特率、数据位、校验位、停止位)
  2. 点击"打开串口"按钮
  3. 在发送区输入数据(文本或HEX格式)
  4. 点击"发送"按钮
  5. 接收区显示返回数据
  6. 使用"清空"、"保存"按钮管理数据

5.4 通信协议示例

方向数据说明
PC→MCU0x01请求温度数据
MCU→PC0x25 0x50温度数据(25.5℃)
PC→MCU0x02请求状态信息
MCU→PC"System OK"状态信息
PC→MCU其他回显数据

六、常见问题解决

6.1 无法打开串口

  • 检查串口号是否正确
  • 确认没有其他程序占用串口
  • 检查串口线连接是否正常
  • 尝试降低波特率

6.2 数据乱码

  • 检查波特率是否匹配
  • 确认数据位、校验位、停止位设置一致
  • 检查是否有干扰源
  • 尝试使用较低的波特率

6.3 接收数据不完整

  • 增加接收缓冲区大小
  • 检查RThreshold设置(建议1-10)
  • 使用轮询方式补充接收
  • 检查硬件流控制设置

七、扩展功能

7.1 添加数据校验

// 在发送数据前添加校验和
BYTE checksum = 0;
for (int i = 0; i < len; i++) {
    checksum ^= data[i];
}
// 将校验和附加到数据末尾

7.2 添加自动发送功能

// 在OnInitDialog中添加定时器
SetTimer(1, 1000, NULL); // 1秒定时

// 添加定时器处理
void CSerialCommDlg::OnTimer(UINT_PTR nIDEvent) {
    if (nIDEvent == 1 && m_mscom.GetPortOpen()) {
        // 自动发送请求
        BYTE cmd = 0x01;
        COleVariant var(cmd);
        m_mscom.SetOutput(var);
    }
    CDialog::OnTimer(nIDEvent);
}

7.3 添加数据绘图功能

// 添加绘图控件
#include <ChartCtrl.h> // 使用第三方图表控件

// 在接收数据中更新图表
CChartCtrl m_chart;
m_chart.AddPoint(x, y); // 添加新数据点
m_chart.Invalidate();

八、总结

本程序使用VC++ MFC对话框实现了PC与单片机的串口通信,具有以下特点:

  1. 完整的通信功能

    • 支持多种串口参数配置
    • 支持文本和十六进制双模式
    • 支持数据发送、接收、显示和存储
  2. 用户友好界面

    • 直观的参数配置
    • 清晰的接收显示区
    • 实时的状态反馈
  3. 稳定的通信机制

    • 基于MSComm控件的中断接收
    • 完善的错误处理
    • 数据校验机制
  4. 良好的扩展性

    • 可添加自定义通信协议
    • 可集成数据分析和可视化
    • 可扩展为多设备通信平台