软件工程的核心在于管理复杂性。
Software engineering is about managing complexity

原始链接: https://hack8s.com/422/once-again-software-engineering-is-about-managing-complexity

软件工程的核心往往被误解:虽然人工智能擅长编写代码(将想法转化为计算机指令),但它并不能取代**构建软件**这一行为。工程实践的核心在于权衡取舍、做出架构决策以及平衡业务限制——这是一个需要深刻的、以人为本的语境的过程。 人工智能是生成样板代码、测试和文档的强大加速器,但存在一种危险的倾向,即把工程判断权交给缺乏关键语境的模型。现实世界的系统受到团队技能、运维历史、财务风险和不断变化的产品需求等微妙因素的影响。由于人工智能可以低成本地生成代码,我们面临着这样一种风险:即系统中充斥着团队并未真正掌握或理解的代码,从而增加了系统的“认知表面积”。 在这一新形势下,算法思维和系统设计比以往任何时候都更加重要。我们必须将关注点从基于代码量的“生产力指标”转移到系统复杂性的有效管理上。归根结底,人工智能应被视为一种助手,它让我们能够腾出更多时间用于高层级的工程判断,从而确保我们始终对自己交付的系统负责。

该 Hacker News 帖子讨论了人工智能在软件工程中的作用,其契机源于文章《软件工程旨在管理复杂性》。这场讨论反映了业内观点存在的严重分歧: * **怀疑论者**认为人工智能并非必不可少,并指出软件工程在 AI 出现之前就已蓬勃发展。他们主张,将编程与架构设计剥离会损害工程师的思维模型,导致“地图与疆域”之间出现危险的脱节。批评者进一步表示,过去的代码质量优于当今产出的代码。 * **支持者**坚持认为,人工智能在生成系统架构和通过澄清问题来优化需求方面的能力,已经优于许多人类同行。他们预测系统架构领域即将迎来“Claude Code 时刻”,并预期 AI 很快将在此领域超越人类。 总体而言,这场讨论凸显了传统观念——即深入的实践编程对严谨工程至关重要——与新兴观点——即 AI 可能很快在复杂架构任务中超越人类能力——之间的冲突。
相关文章

原文

There is a misunderstanding about software engineering that AI is making increasingly visible: we tend to confuse writing code with building software.

There is some overlap in certain aspects, but they’re not the same thing.

Writing code means translating an idea into instructions a computer can execute. Building software means deciding which instructions should exist in the first place, how they should interact, which constraints matter, what’s the cost of the decisions, which tradeoffs can be considered acceptable, and how the resulting system can evolve without collapsing under its own constraints and limitations.

Let’s start from a basic premise: AI is an essential tool because it is remarkably good at the first problem.

The second is where software engineering actually begins.

The difficult part was never typing the code

Consider a relatively ordinary engineering requirement.

We need to process incoming events and update some data.

And these are some of the first questions that arise during a technical discussion:

  • Should we process them synchronously?
  • Should we put them into a queue?
  • Do we need exactly-once processing, or is at-least-once sufficient?
  • Can the system tolerate eventual consistency?
  • What happens when processing fails halfway through?
  • Should we retry? How many times?
  • What happens if the consumer is unavailable for three hours?
  • Do events need to preserve ordering?
  • How much traffic do we expect today? What about two years from now?
  • What are the consequences if an event is processed twice?

… and so on.

These questions have very little to do with syntax.

Programming language’s choice matters, because affects team’s fluency, team’s performance, system’s performance, safety, maintainability, tooling, and operational characteristics, but it does not answer the fundamental questions.

The difficult part is choosing the architecture that represents the right set of compromises.

And there is rarely a universally correct answer.

1 problem can have N completely different correct solutions

This is particularly obvious when software exists inside a business.

Imagine two companies asking their engineering teams to build what sounds like exactly the same feature.

Their requirements may look identical on paper.

But:

  • Company A has 500 users while Company B has 20 million.
  • Company A may have three engineers maintaining the system. Company B may have 200.
  • One may require strong consistency because mistakes have serious financial consequences. The other may happily accept eventual consistency in exchange for availability and throughput.
  • One company may need to ship in three weeks.
  • Another may expect the system to remain operational for fifteen years.
  • One may already have Kafka, Kubernetes, PostgreSQL, observability infrastructure, and engineers experienced with distributed systems. Another may have a single application server and a PostgreSQL database maintained by four developers.

The technically impressive solution for one company could be an irresponsible solution for the other.

This is why architecture cannot be reduced to asking:

What is the best way to implement X?

The correct question is usually closer to:

Given these constraints, this team, this business, this infrastructure, this budget, these risks, and the expected evolution of the product, what is the most appropriate way to implement X, today?

That is a radically different question.

AI generates solutions. Engineers own tradeoffs.

This distinction is becoming increasingly important because of how AI is being adopted by software organizations.

AI is extraordinarily useful for software development. We use it as an accelerator: generating boilerplate, exploring APIs, proposing implementations, finding potential bugs, explaining unfamiliar code, producing tests, comparing approaches, or simply reducing the amount of mechanical work required to turn an idea into working code.

But there is a dangerous tendency to extend this capability into something much broader:

delegating engineering judgment itself

You can:

  • give an AI model a requirement and ask it to design the system, “no mistakes”: It will design one.
  • ask it to choose a database: It will choose one.
  • ask whether you should introduce a queue, microservices, caching, CQRS, event sourcing, Kubernetes, Redis, or another abstraction: It will give you an answer.

The existence of an answer, however, does not mean the underlying engineering problem has been solved.

The real problem is that the correct decision depends on context, often an enormous amount of context, that needs hours, days, or even weeks to analyze, understand, and evaluate, often involving multiple departments within the same organization, and frequently leaving gray areas that are difficult to formalize and may become a challenge when future changes are required.

This is how real world works: some of that context exists in documentation. Much of it does not.

It exists in conversations with customers. In the history of the product. In the skills of the engineering team. In operational incidents from three years ago. In budget constraints. In deadlines. In contractual obligations. In the weird behavior of a legacy system nobody wants to touch. In knowing that a customer is likely to request a particular feature six months from now.

And sometimes it exists simply in experience: recognizing that a theoretically elegant architecture will become an operational nightmare for the team expected to maintain it.

You cannot treat all of this as a small detail that will somehow be captured by adding another paragraph to an AI prompt.

There is no architecture without tradeoffs

Engineering is largely the discipline of deciding which problems you are willing to have. Even if you don’t work at FAANG scale, your projects may still need to process enough data that making the right engineering decisions becomes essential to maintaining adequate performance, without having to throw expensive hardware at systemic inefficiencies.

Even “simple” choices come with factors that need to be considered:

  • Normalize the database aggressively, and you may gain integrity while making certain queries more complicated.
  • Denormalize it, and you may improve read performance while increasing synchronization complexity.
  • Add caching, and you reduce latency while introducing invalidation problems.
  • Introduce asynchronous processing and you decouple components while creating new failure modes.
  • Split a monolith into microservices and you gain independent deployment boundaries while introducing distributed-system complexity.
  • Optimize aggressively and you may gain performance while sacrificing readability.
  • Abstract aggressively and you may reduce duplication while making the system harder to understand.
  • Avoid abstraction and the opposite problem eventually appears.

None of these can be evaluated independently. Every optimization spends complexity somewhere. The job of an engineer is therefore not simply to make the system work.

It is to decide where complexity belongs.

Algorithmic thinking matters more, not less

This is why I think algorithmic thinking becomes more important in the age of AI, not less.

And by algorithmic thinking I do not simply mean knowing textbook algorithms or being able to implement a red-black tree from memory.

By algorithmic thinking, I mean defining a set of basic rules to follow and applying everyday. I’ve put together a ten-point set of principles:

  1. Decompose the problem.
  2. Identify invariants.
  3. Understand data flows.
  4. Choose appropriate data structures.
  5. Reason about time and space complexity.
  6. Identify contention.
  7. Understand failure modes.
  8. Recognize which operations are expensive and which ones actually matter.
  9. Separate essential complexity from accidental complexity.
  10. Understand what should happen when assumptions stop being true.

I know, in the time of boosted productivity, this can sounds weird. AI can generate ten implementations in the time it previously took an engineer to write one. The challenge is to use AI as a tool to apply these principles through targeted, controlled iterations, while keeping a human in the loop.

That makes choosing the right implementation ten times more important. This is what boosted productivity really means.

Your programming language is a tool, not an identity

The same applies to programming languages.

There is value in choosing the right tool for the job. Languages have different strengths, ecosystems, performance characteristics, type systems, concurrency models, deployment models, and safety guarantees.

Those differences matter, but engineering discussions sometimes give programming-language choice “overvaluation”. In many business systems, choosing between two reasonable languages will matter considerably less than choosing the wrong architecture, the wrong data model, or the wrong operational strategy.

Pick a language appropriate for the scenario. If several languages are appropriate, there is significant value in choosing the one your team understands deeply. Don’t base your decisions on the assumption that LLMs perform better with one PL or another. Always put the team first.

The 5-matters rules are:

  • Fluency matters.
  • Ecosystem maturity matters.
  • Maintainability matters.
  • Debugging matters.
  • Deployment matters.

This is why, today more than ever, with LLMs at our disposal, I encourage engineers not to be religiously loyal to a single PL, but to become as polyglot as possible, learning to use AI assistants as learning tools, not merely as code generators, always double checking official docs.

The real optimization target is the lifetime of the system

Another mistake is optimizing software exclusively for its current requirements.

Modern software cannot be static, and it must be built to respond to following changes:

  • Users
  • Customers
  • Traffic
  • Business models
  • Regulations
  • Teams
  • Infrastructures

Every point has a weight that can change overtime. So the feature that appears insignificant today can become the core workflow two years later.

Good engineering therefore requires some ability to anticipate change without falling into the opposite trap of designing elaborate abstractions for hypothetical futures that may never arrive, just because teams accepted some codex output, too long to be fully reviewed. That balance is difficult, because:

  • How likely is this requirement to change?
  • Where should we deliberately introduce flexibility?
  • Where should we keep things simple?
  • Which technical debt is acceptable?
  • Which technical debt could become structurally expensive?
  • What should we optimize now?
  • What should we deliberately leave unoptimized until measurements prove that it matters?

This is engineering judgment.

There is no AI that can correctly answer these and many other questions, nor can you simply put an AI agent to work on them and expect a reliable outcome. There is no prompt that makes those questions disappear.

AI-generated code creates a new ownership problem

There is another issue that I think software organizations are underestimating: codebase ownership.

AI makes producing code extraordinarily cheap. That sounds entirely positive until you remember that producing code and maintaining code are two different economic activities. An engineer can now generate hundreds or thousands of lines of apparently reasonable code extremely quickly.

A team can merge them, tests pass, the feature works, productivity appears to have increased, but something critical may have been lost. And if you want to understand what, ask yourself:

  1. Does the team actually understand the general architecture?
  2. Do they understand the data structures?
  3. Do they understand why a particular abstraction exists?
  4. Do they understand the failure behavior?
  5. Do they know which assumptions the generated implementation depends on?
  6. We can assume they could modify it confidently six months later without asking another AI to explain the code that AI previously generated?

If the answer is no, the team has not eliminated complexity. It has probably introduced others, which become increasingly relevant over time.

Lines of code are cheap. Understanding is expensive.

This is perhaps the biggest change AI introduces into software economics.

Historically, producing code was relatively expensive. Now it is becoming extremely cheap. While understanding a system, it’s becoming increasingly expensive

That distinction matters: a pull request containing 3K lines of AI-generated code is still a 3K-line addition to the cognitive surface area of your system. And no, a passing-test suite is not the answer, because it gives you a false sense of confidence that everything’s under control because the system -apparently- works as intended.

If you ship it, you own it

For this reason, I believe strong technical management should maintain a very simple principle:

Never ship code you do not own.

What do I mean by “ownership”? Having a complete understanding of the codebase, having validate every part of it.

AI-generated code should not receive a lower standard of scrutiny because it was generated quickly with some advanced/expensive/frontier/whatever LLM model.

The challenge now is to determine whether (and when) it makes sense to invest time in reviewing such large pull requests, or whether it is better to move more slowly through smaller iterations, where small chunks of application logic are developed through a controlled human–AI exchange.

Software engineering may actually be getting harder

As a final point, I want to focus on an interesting paradox that seems to have emerged recently:

AI makes programming easier while potentially making software engineering harder.

We can produce more code, faster than ever before. We can create prototypes almost instantly. We can integrate unfamiliar libraries without spending days reading documentation. We can generate tests, APIs, database layers, infrastructure configuration, frontend components, migrations, and entire services at extraordinary speed.

But our capacity to understand systems has not increased at the same rate. And this creates a new bottleneck, which is no longer:

How quickly can we write this?

But:

How much complexity can this organization actually understand and control?

Productivity metrics we used so far, are broken: a team producing five times more code is not necessarily five times more productive. We haven’t been using AI systems in software engineering long enough to have reliable data yet, but it is becoming increasingly clear that the initial productivity gains we saw during the early stages of LLMs adoption, are now coming at a cost in terms of maintaining and evolving moderately complex systems.

From now on, productivity should therefore not be measured by how much software we generate.

It should be measured by how effectively we solve problems while keeping the resulting complexity under control.

AI should increase engineering leverage, not replace engineering judgment

None of this is an argument against AI-assisted development. Quite the opposite. AI is probably the most powerful productivity tool software engineers have received in decades.

But we should use that leverage to spend more time on engineering, not less. We’re allowed to let AI write the repetitive adapter, generate the first version of the test suite, explain an unfamiliar library. It’s extremely useful to let it help investigate an error, propose 3 implementations or refactor mechanical code. It’s a time saver, a booster, an assistant, combined.

Code was never the product

Software engineering is not merely an academic discipline. We are here to serve our users, so it is a boots-on-the-ground practice, and it needs to remain connected to a real world that is changing faster than ever. It is up to your team to decide how, where, and when AI-generated code fits into your development process.

Programming languages will evolve. Frameworks will disappear. AI models will become dramatically more capable. The cost of generating code will continue approaching zero.

But complexity will remain. It belongs to engineers who can understand the most complexity while introducing the least unnecessary complexity of their own.

AI can help us get there. But it cannot relieve us of our responsibility for what we ship.

联系我们 contact @ memedata.com