When it comes to AI coding, I think we should freely use any technology that works. Both the speed of implementation and the number of ideas we can try have changed dramatically compared to when humans wrote all the code themselves. Tasks that were once measured in hours can now be completed in a short amount of time.
On the other hand, just because more code is being generated doesn’t mean humans can review it at the same pace. Trying to keep the cycle going with “if AI writes a lot, humans must read a lot” is unsustainable.
It’s beneficial to create three prototypes and compare them, but if all three end up being merged into production, the work of verifying the impact of changes triples. The increase in implementation speed is not the same as the increase in speed at which we can judge which changes are acceptable. I don’t intend to close this gap solely by relying on reviewers’ extra effort.
What matters, then, is not abandoning review altogether but building a structure that maintains quality while reducing dependence on humans checking every line of code.
Define the boundaries of specifications and responsibilities first
Even if you entrust implementation to AI, you can’t verify anything if what you’re supposed to build remains vague. You need to decide in advance how the system will be partitioned, what each component is responsible for, and where data flows from and to. You should also establish the boundaries between normal and abnormal states up front.
For example, simply saying “create a queue” doesn’t clarify whether the same message may be delivered twice, when a failed operation should be retried, or whether the order must be guaranteed. Even if the implementation appears to work, you can’t tell whether it’s correct. Delegating the choice of implementation to AI is one thing; implicitly leaving even the criteria for judgment to AI is another.
Even if you decide that “failed messages should be retried,” the operational behavior changes depending on whether you retry indefinitely or, after a certain number of attempts, send the message elsewhere. When a process times out, the remote side might already have succeeded. If you proceed with implementation without clarifying these points, it becomes ambiguous later whether duplicate deliveries should be treated as a bug or as intended behavior.
What we need up front isn’t a massive design document. We need to understand the input‑output contracts, the boundaries of responsibilities, how failures are handled, and which existing behaviors must not be altered. Diagrams, short prose, or concrete examples are all acceptable. I want to emphasize that humans and AI should share the same premises, and that the artifacts should be structured so they can be verified later.
We can’t write a complete specification from the outset. Ambiguities often surface as we build. But when that happens, we should explicitly note which assumptions have changed and revert to the tests or boundary definitions. We don’t want to end up looking at AI‑generated code later and trying to infer the specification from it.
Dividing the system at this stage is also important. If a single change touches authentication, storage formats, and UI rendering, you’ll lose sight of what needs to be protected even before you’ve read the diffs produced by AI. When boundaries are clear, you can limit the values passed into and returned from each component and restrict the side effects they may have. This lets humans retain an overall view while delegating the detailed implementation.
Simply increasing the volume of reviews won’t keep up
Code review is valuable. It checks whether the code adheres to the design, whether it’s maintainable for readers, and whether any conditions missed by the tests have been overlooked. There are things that only a human can notice after reading the code.
However, as AI coding raises the amount of code that needs to be implemented, treating every line with the same level of scrutiny becomes a bottleneck. If you cut down on reading time to eliminate review backlogs, you’ll end up missing more issues. Human attention cannot be increased indefinitely.
Moreover, having read the diff does not necessarily mean you understand the behavior. Even a few lines of change may require examining their relationship to callers or persisted data before you can judge them correctly. Conversely, for small, localized changes where the types are clear and the tests are already in place, it can be more effective to verify the contract and behavior rather than step through each generated line. Allocating review time solely based on line count isn’t a particularly good metric.
Therefore, I think the role of code review will shift. Instead of placing the entire burden of quality assurance on a single review, we should first have machines verify what can be checked automatically. Human reviews should concentrate on aspects that are hard to determine through mechanical inspection alone—such as interpreting the specification, defining responsibility boundaries, and making security decisions.
The risk level of changes isn’t uniform. You don’t need to treat a simple wording tweak the same way you would handle authentication or data‑deletion logic. For the latter, retain stronger human oversight, and if needed, separate execution and deployment permissions. “Humans not reading every line” is not the same as “humans not taking responsibility.”
In particular, boundaries of permissions, handling of money or personal information, data migrations, and changes to APIs exposed to external systems are hard to revert once a mistake is made. In such cases, you need to consider not only the diff review but also the behavior before and after the change, whether it can be rolled back on failure, and who approves it. To distinguish where human judgment can be omitted from where it must remain, designing the system so that the impact of any change stays small is essential.
Reviews should extend beyond code
I think it’s better not to define “review” solely as the act of reading the generated diffs. We need to verify whether the specifications set at the outset actually match the real problem, whether the tests can extract the important conditions from those specifications, and whether the resulting functionality truly supports the intended work. There are other items to check both before and after reading the code.
For example, when a human examines test cases proposed by AI, they should ask, “Is this assert merely copying the implementation’s result?” and “Is the expected outcome defined for failure cases?” Even if the code contains a bug, the test will pass if the expectation is written to match that buggy behavior. In some situations, questioning the test’s criteria is more valuable than stepping through the implementation line by line.
Even when reviewing specifications, we should look not only at what is written but also at the boundaries that aren’t documented. We need to consider not only what happens with valid inputs but also how the system behaves under concurrency, partial failures, and re‑execution. While enumerating every possible scenario in advance is impossible, identifying risky boundaries and raising questions about them is something humans should spend time on.
In other words, the goal isn’t merely to cut down human review time, but to change how that time is spent. Rather than reading code in proportion to its line count, we should allocate review effort based on the potential impact of a mistake and on judgments that are hard for a machine to determine. To that end, we should first automate the parts that can be verified mechanically.
Adding verification beyond code reviews
If we reduce the amount of code that needs to be read, we must provide alternative ways to detect errors. This includes type checking, linters, static analysis, unit tests, integration tests, end‑to‑end tests, security scans, property‑based testing, and fuzzing. Combine these different kinds of checks in the CI pipeline.
For example, even if a unit test examines all the branches of a single function, it does not guarantee that data passed between components is correct. Likewise, an end‑to‑end test that exercises one normal workflow does not ensure that all boundary values and exception cases are handled properly. You need to catch different kinds of failures across multiple layers.
Take the queue example: a unit test can verify the retry count, while an integration test can confirm the interaction with the storage destination. Yet you still need a separate test to see what happens if you stop the process midway and then restart it. Tests that feed large volumes of input or malformed data uncover issues distinct from ordinary success cases. The appropriate checks vary depending on what you’re worried about.
Increasing the number of test cases is not the goal in itself. Running hundreds of normal‑case tests won’t tell you whether duplicate processing can be prevented. Conversely, if the property you must protect is “re‑executing the same operation does not increase the result,” directly verifying that property is far more meaningful. We should extract such properties from the specification, turn them into tests, and verify them from a perspective separate from the implementation’s internal structure.
Using AI to perform code reviews or to check consistency with the specification is also a viable approach. You can even compare results across multiple models. However, if the generated code and the generated tests are based on the same misunderstanding, they will both pass together. AI‑driven cross‑review does not constitute an independent proof of correctness. If possible, we should base verification on expectations derived from the specification, existing behavior, or observable properties from external sources.
For example, suppose you tell AI, “Make failed operations safely retryable,” and have the AI produce both the implementation and the tests. If the AI assumes that “a timeout means the operation never occurred,” both the code and the tests will be built around that premise. A green CI build does not prove that premise is correct. The question of what counts as correct remains separate from the code‑generation process.
Beyond the mere fact that “the test passed,” we need to understand which guarantees the test is verifying and which it is not. Ultimately, only a human who truly grasps the problem can determine that.
Don’t ask LLMs for things that should be decided mechanically
Expanding the scope of AI usage is not the same as having everything solved by an LLM.
Whether a format is broken, whether types match, or whether there are known issues with dependencies—these are things that can be checked more quickly, cheaply, and reproducibly with existing tools. There’s no need to revert to LLM judgments each time for matters that can be resolved with regular expressions, parsers, or similar mechanisms.
By “solving things mechanically” here, I don’t mean replacing everything with a brand‑new AI system. Let the compiler check types, the parser verify syntax, and the test runner compare inputs and outputs. By assigning each tool its specialty, you can reproduce failures under the same conditions for the same input, and the reasons for the failures become easier to trace later.
Furthermore, by fixing input and output formats with types or schemas, you can make it harder to create invalid states. This shifts potential runtime problems into constraints handled during design or build time. If you can make more decisions without relying on an LLM, the system’s operation will become more stable.
For example, using verified types for data that crosses boundaries reduces the need to constantly ask, “Is this value really allowed?” in subsequent processing. You can also treat failures as explicit errors rather than silently substituting them with vague default values. This isn’t about boosting AI’s capabilities; it’s about shaping the system so that AI implementations are less likely to err and, when they do, the errors are easier to detect.
LLMs are best suited for tasks such as organizing ambiguous requirements, generating multiple implementation proposals, or formulating investigative hypotheses while moving back and forth between logs and code. However, you should verify those hypotheses as much as possible through actual execution results or clear testing.
Keep changes small so you can pause or roll them back
Even after passing verification, there are problems that won’t become apparent until the code is actually used. In such cases, it’s problematic if a large change is introduced all at once, spreads immediately to all users, and cannot be reverted. If AI coding enables changes to be made quickly, there also needs to be a safeguard on the side of the person deploying those changes.
Break changes into functional units, provide a period for reading legacy data formats, roll out updates incrementally, and, if necessary, allow features to be disabled. Whether these measures are feasible depends on the system, but you should consider what can be rolled back before implementation. When dealing with operations that modify data, simply reverting to an older version of the code may not restore the original state.
Whether a review can be made lightweight isn’t determined solely by the sheer number of tests. Changes that have a narrow impact scope, can detect anomalies, and can be undone are easier for humans to evaluate. Conversely, a process that deletes large amounts of data in a single execution must be handled cautiously, even if the code is short.
Make it possible to trace issues when they arise
No matter how much verification you perform, problems will never disappear entirely. As the amount of code generated by AI increases, it becomes even more important to create a state where post‑occurrence investigation is feasible.
Structured logs, error context, request IDs, traces, metrics, deployment history—these should all be retained so that one can later trace which changes the AI made and which tests were run. When an incident occurs, starting the investigation with “something was changed recently” is different from knowing exactly which version and which processing step the failed request went through; the latter leads to a more focused investigation.
For example, when retries increase, merely seeing a “failure” entry in the logs doesn’t narrow down the cause. You need to identify which message failed, at which processing stage, and on which attempt. If you can overlay the most recent deployment onto that information, you can begin to determine whether the problem stems from faulty input, an external service outage, or a regression introduced by the change. This reduces the need to read the entire codebase each time to hypothesize about the issue.
The same applies to an AI’s work history. Simply noting that “the AI fixed it” isn’t enough. You need to understand which problem it intended to fix, which files were altered, and what actions were taken that led to the conclusion that it succeeded. If the code diffs are linked to the verification results, another person can later audit that decision. It’s more important to preserve the rationale behind the chosen fix than to store an unlimited conversation log.
Observability is not a substitute for preventive measures. Adding more logs won’t eliminate bugs, and you can’t indiscriminately record sensitive information. Still, even when no one has memorized every implementation detail, the system should be designed so that observed phenomena can be traced back to the relevant layer.
Understand the underlying principles rather than memorizing the implementation
In AI coding, the amount of code you haven’t written yourself grows. It’s difficult to keep track of every implementation detail, and I don’t think that’s a goal worth pursuing.
On the other hand, the ability to reason about what the problem is, why it occurs, and which layer to suspect is indispensable. When investigating a database failure, you don’t need to memorize the entire ORM implementation. However, without understanding transactions, locks, isolation levels, indexes, and query plans, you may misinterpret the meaning of delays or inconsistencies.
This isn’t limited to databases. You also need to grasp the assumptions underlying memory management, concurrency, network retries, writes to the file system, and other aspects of software behavior. Even if you only remember how to call a library, you won’t know what will happen when those assumptions break down.
When I encounter a bug, I don’t want to settle for merely noting that it was fixed. I examine the observed symptoms, verify how the related processes should behave, and try to understand under what conditions the underlying assumptions were violated. I narrow down the reproduction conditions, create tests that match my hypothesis, and compare the results before and after the fix. Sometimes I can’t prove the cause definitively, in which case I keep the observed facts and my conjectures separate.
For example, in the Boa garbage‑collector reference‑retention bug (see /blog/2026-09-25/), the issue initially appeared as a process crash in a particular environment. What actually needed to be investigated, however, was which values were required until when and how the GC determined what was still alive. The place where the symptom manifests is not necessarily where the cause lies. (source)
Even if the fix itself is small, I want to understand why the change was necessary, under what conditions the bug occurs, and what the tests are preventing. When the same symptom appears elsewhere, I don’t want to simply apply the previous fix; I want to be able to re‑examine the cause based on the underlying principles.
Of course, when you encounter an unknown bug, you won’t always be able to explain the underlying principle on your own from the outset. You may need to chase logs and code together with the AI, generating hypotheses collaboratively. For mechanisms you don’t understand, you ask the AI to explain them just as you would ask a teacher. By asking questions like “Why does it work that way?” and “If that explanation is correct, under what conditions could it be reproduced?” you deepen your own understanding.
However, you should not simply adopt the AI’s explanation as the cause without verification. You need to cross‑check it against the actual code, specifications, and reproducible tests. Analyzing with the AI is different from handing the decision‑making over to the AI.
If you understand the underlying fundamentals, you can focus your questions to the AI—e.g., “Could this be an issue with mutual exclusion?” or “How does the query plan differ before and after the change?”—and narrow down what to investigate. Rather than just reading the explanation, you can also corroborate it with logs and execution plans. Conversely, if you lack knowledge of the assumptions, it’s hard to recognize why a seemingly plausible fix might actually be dangerous.
The more AI assists with implementation, the more crucial it becomes to understand the fundamentals of computer science and the specific operational principles of the system. Even if an AI’s explanation sounds convincing, a different set of assumptions can lead to a different conclusion. Recognizing that discrepancy requires knowledge beyond simply reading code. At the very least, for the parts where a problem occurs, we want to be able to explain not only that the fix passed the tests but also why the bug happened and why that fix prevents it.
Deciding what to use is a human responsibility
Another point is that there are aspects that cannot be settled solely by specifications or tests. It concerns whether the resulting product is something the user—or you—actually wants to use.
Even if it behaves exactly as required and passes all tests, it may still be cumbersome to operate or subtly different from what was desired. This judgment is separate from the correctness of the implementation. While you can ask AI to propose alternatives or point out potentially awkward parts, only a human can verify, by actually using it, how it feels and whether the effort is worth the users’ time.
For example, even a screen that saves every input field and returns the correct result when a button is pressed can be unusable if users have to re‑enter the same fields each time they perform the task. Unit tests for the individual functions may pass, and the UI may match the specification. Yet that doesn’t guarantee that the work the user truly wants to do has become easier.
Making that judgment requires actually trying the workflow. You need to see where users get confused, where they are forced to wait, and whether the process is genuinely simpler than the existing one. Some aspects can be measured numerically, but choosing which metrics to collect itself requires judgment. While you can ask AI to generate alternative designs, it cannot automatically determine what feels cumbersome or what aspects are most important.
If AI coding enables you to produce more code, you won’t simply adopt everything as‑is; you’ll also need to evaluate it and decide what to keep and what to discard. Determining what to build is only part of the job—confirming that the finished product truly meets what you want is also human work.
Summary: Where human responsibility remains
Even as the scope of tasks delegated to AI coding expands, human responsibility does not disappear. First, you must decide whose problem you are solving and what that problem is. Define the system’s responsibilities, specify what constitutes correct behavior, and identify which failures are unacceptable. If this is unclear, even rapid code generation by AI will lack a basis for evaluating its results.
Next, you need to establish how to verify those criteria. Use types and static analysis to eliminate what can be caught early, and let tests confirm the properties that are critical to the specification. Human reviews should not be aimed at reading every line with the same intensity; instead, they should focus on areas that require judgment—such as interpreting the specification, handling dangerous edge cases, and defining test evaluation criteria. Even if a test written by AI passes, you should not assume that the test’s expected outcomes are themselves correct.
Nevertheless, problems will still arise. I think it is our responsibility to create a situation where we can track the phenomena when they occur and be able to halt or revert changes as needed. Rather than trying to eliminate the need to read code, we should ensure that, when a problem happens, we can quickly locate the relevant parts. Moreover, based on the underlying principles and the actual behavior, I want to understand how a bug originated before evaluating the explanations and fixes proposed by AI.
Finally, we must decide whether the finished product is worthwhile to use. Even if it works exactly as specified, if it’s unwieldy we’ll rebuild it. Because we can now produce many prototypes, decisions such as discarding a design, trimming features, or shifting direction become equally important.
I don’t aim to comprehend every single line of code generated by AI. Instead, I want to maintain a state where I can determine what we’re building, verify its correctness, and assess whether it actually meets our needs. When a problem occurs, I strive to understand the underlying principles at work and where an assumption has broken down. Creating a system that lets us control the software without reading every line of code, and understanding how to use that system, is what I believe will be the role of humans going forward.